Skip to content

Commit de13253

Browse files
committed
feat: configurable content routing via FEED_SOURCES variable
Introduce a FEED_SOURCES repository variable that controls which content sources appear in the main chronological feed vs. as sidebar panels. Source keys: writing, channel, playlists, hn_stories, hn_comments Default: writing,channel,hn_stories (previous hardcoded behaviour) generate.py: - Add _ALL_SOURCES, _DEFAULT_FEED_SOURCES, _SOURCE_META constants - Add _parse_feed_sources() — accepts comma or newline-separated keys, ignores unrecognised tokens, falls back to default if result is empty - generate_site() gains feed_sources: frozenset[str] param - Feed and sidebar are now both derived from the same routing table: feed_posts = union of posts for keys in feed_sources, sorted newest-first sidebar_panels = list of typed panel dicts for keys NOT in feed_sources Playlists still get one panel per playlist ID; all others get one panel - main() reads FEED_SOURCES env var and passes parsed value to generate_site - config_ctx now includes feed_sources list for the config page template index.html: - Sidebar replaced with a generic {% for panel in sidebar_panels %} loop that type-dispatches on panel.type for correct item rendering: hn_comments → story-title + excerpt link hn_stories → article link + score/comment count playlist/channel → thumbnail video items (anything else) → generic title + date config.html: - 'Section' column renamed to 'Destination' - Each row now shows Feed or Sidebar based on feed_sources context var - HN row shows nuanced label (Feed/Sidebar per sub-type) when stories and comments are split across destinations
1 parent f1772a1 commit de13253

3 files changed

Lines changed: 161 additions & 67 deletions

File tree

blog/generate.py

Lines changed: 111 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
Required to enable the "My Reading" section.
2323
Set this as a GitHub Actions repository variable
2424
(Settings → Variables) so it is NOT stored in source.
25+
26+
FEED_SOURCES Optional. Comma- or newline-separated list of source
27+
keys to include in the main chronological feed.
28+
Everything else automatically becomes a sidebar panel.
29+
Valid keys: writing, channel, playlists, hn_stories, hn_comments
30+
Default: writing,channel,hn_stories
2531
"""
2632

2733
import os
@@ -60,6 +66,44 @@
6066

6167
_PAGE_SIZE = 10
6268

69+
# ---------------------------------------------------------------------------
70+
# Content routing
71+
# ---------------------------------------------------------------------------
72+
73+
# Ordered list of all recognised source keys.
74+
_ALL_SOURCES = ("writing", "channel", "playlists", "hn_stories", "hn_comments")
75+
76+
# Which sources appear in the main chronological feed by default.
77+
_DEFAULT_FEED_SOURCES: frozenset[str] = frozenset({"writing", "channel", "hn_stories"})
78+
79+
80+
def _parse_feed_sources(env_val: str | None) -> frozenset[str]:
81+
"""Parse FEED_SOURCES env var into a frozenset of valid source keys.
82+
83+
Accepts comma- or newline-separated values. Unrecognised tokens are
84+
silently ignored. Falls back to _DEFAULT_FEED_SOURCES if the result
85+
would be empty.
86+
"""
87+
if not env_val:
88+
return _DEFAULT_FEED_SOURCES
89+
parsed = frozenset(
90+
tok
91+
for raw in re.split(r"[\r\n,]+", env_val)
92+
for tok in [raw.strip().lower()]
93+
if tok in _ALL_SOURCES
94+
)
95+
return parsed if parsed else _DEFAULT_FEED_SOURCES
96+
97+
98+
# Human-readable labels for each source key (used in config page + sidebar headings).
99+
_SOURCE_META: dict[str, dict] = {
100+
"writing": {"title": "My Writing", "icon": "✍️"},
101+
"channel": {"title": "My Videos", "icon": "🎥"},
102+
"playlists": {"title": "My Watching", "icon": "📺"},
103+
"hn_stories": {"title": "HN Submissions", "icon": "🗞️"},
104+
"hn_comments": {"title": "HN Comments", "icon": "💬"},
105+
}
106+
63107
# ---------------------------------------------------------------------------
64108
# Static asset helpers
65109
# ---------------------------------------------------------------------------
@@ -100,8 +144,11 @@ def generate_site(
100144
youtube_playlist_ids: str | None = None,
101145
youtube_channel_ids: str | None = None,
102146
hn_usernames: list[str] | None = None,
147+
feed_sources: frozenset[str] | None = None,
103148
) -> None:
104149
_start = time.monotonic()
150+
if feed_sources is None:
151+
feed_sources = _DEFAULT_FEED_SOURCES
105152

106153
repo_owner = repo.split("/")[0]
107154
repo_name = repo.split("/")[-1]
@@ -189,19 +236,11 @@ def generate_site(
189236
)
190237

191238
# --- Sidebar data ---
192-
# Split HN posts: stories go into the main feed, comments go in the sidebar
193239
_SIDEBAR_LIMIT = 5
194240
hn_stories = [p for p in reading_posts if p.get("metadata", {}).get("hn_type") == "story"]
195241
hn_comments = [p for p in reading_posts if p.get("metadata", {}).get("hn_type") == "comment"]
196242

197-
# Unified main feed: writing + channel videos + HN stories, newest first
198-
feed_posts = sorted(
199-
writing_posts + channel_posts + hn_stories,
200-
key=lambda p: p["created_at"],
201-
reverse=True,
202-
)
203-
total_pages = max(1, (len(feed_posts) + _PAGE_SIZE - 1) // _PAGE_SIZE)
204-
# Build per-username HN profile links (use first effective username if multiple)
243+
# Build per-username HN profile links
205244
_hn_user = (effective_hn_usernames or [None])[0]
206245
hn_submitted_url = (
207246
f"https://news.ycombinator.com/submitted?id={_hn_user}" if _hn_user else None
@@ -213,30 +252,67 @@ def generate_site(
213252
f"https://news.ycombinator.com/user?id={_hn_user}" if _hn_user else None
214253
)
215254

216-
# Build per-playlist sidebar panels (up to _SIDEBAR_LIMIT videos each, in playlist order)
217-
playlist_groups: list[dict] = []
218-
_seen_pids: dict[str, dict] = {}
219-
for p in playlist_posts:
220-
src_id = p.get("metadata", {}).get("source_id", "")
221-
if src_id not in _seen_pids:
222-
grp: dict = {
223-
"source_id": src_id,
224-
"view_more_url": p.get("metadata", {}).get("view_more_url", ""),
225-
"posts": [],
226-
}
227-
playlist_groups.append(grp)
228-
_seen_pids[src_id] = grp
229-
if len(_seen_pids[src_id]["posts"]) < _SIDEBAR_LIMIT:
230-
_seen_pids[src_id]["posts"].append(p)
231-
232-
sidebar = {
233-
"hn_comments": hn_comments[:_SIDEBAR_LIMIT],
234-
"hn_threads_url": hn_threads_url,
235-
"hn_profile_url": hn_profile_url,
236-
"hn_username": _hn_user,
237-
"playlist_groups": playlist_groups,
255+
# Map source key → raw post list
256+
_source_posts: dict[str, list[dict]] = {
257+
"writing": writing_posts,
258+
"channel": channel_posts,
259+
"playlists": playlist_posts,
260+
"hn_stories": hn_stories,
261+
"hn_comments": hn_comments,
238262
}
239263

264+
# --- Main feed ---
265+
feed_posts = sorted(
266+
[p for key in _ALL_SOURCES if key in feed_sources for p in _source_posts[key]],
267+
key=lambda p: p["created_at"],
268+
reverse=True,
269+
)
270+
total_pages = max(1, (len(feed_posts) + _PAGE_SIZE - 1) // _PAGE_SIZE)
271+
272+
# --- Sidebar panels (one per source not in the feed) ---
273+
# Playlists each get their own panel; all others get a single panel.
274+
sidebar_panels: list[dict] = []
275+
for key in _ALL_SOURCES:
276+
if key in feed_sources:
277+
continue
278+
meta = _SOURCE_META[key]
279+
if key == "playlists":
280+
# One panel per playlist
281+
seen: dict[str, dict] = {}
282+
grp_list: list[dict] = []
283+
for p in playlist_posts:
284+
src_id = p.get("metadata", {}).get("source_id", "")
285+
if src_id not in seen:
286+
grp: dict = {
287+
"type": "playlist",
288+
"title": meta["title"],
289+
"icon": meta["icon"],
290+
"posts": [],
291+
"view_all_url": p.get("metadata", {}).get("view_more_url", ""),
292+
}
293+
grp_list.append(grp)
294+
seen[src_id] = grp
295+
if len(seen[src_id]["posts"]) < _SIDEBAR_LIMIT:
296+
seen[src_id]["posts"].append(p)
297+
sidebar_panels.extend(grp_list)
298+
else:
299+
view_all = {
300+
"writing": f"{repo_url}/issues",
301+
"channel": None,
302+
"hn_stories": hn_submitted_url,
303+
"hn_comments": hn_threads_url,
304+
}.get(key)
305+
sidebar_panels.append({
306+
"type": key,
307+
"title": meta["title"],
308+
"icon": meta["icon"],
309+
"posts": _source_posts[key][:_SIDEBAR_LIMIT],
310+
"view_all_url": view_all,
311+
# HN-specific extras
312+
"hn_threads_url": hn_threads_url,
313+
"hn_profile_url": hn_profile_url,
314+
})
315+
240316
# --- Jinja2 setup ---
241317
env = Environment(
242318
loader=FileSystemLoader(str(TEMPLATES_DIR)),
@@ -292,7 +368,7 @@ def generate_site(
292368
next_url = f"{base_path}page/{page_num + 1}/" if page_num < total_pages else None
293369
page_html = index_tmpl.render(
294370
feed_posts=page_posts,
295-
sidebar=sidebar,
371+
sidebar_panels=sidebar_panels,
296372
page_num=page_num,
297373
total_pages=total_pages,
298374
prev_url=prev_url,
@@ -344,6 +420,7 @@ def generate_site(
344420
"video_post_count": len(channel_posts),
345421
"playlist_post_count": len(playlist_posts),
346422
"reading_post_count": len(reading_posts),
423+
"feed_sources": list(feed_sources),
347424
}
348425
config_tmpl = env.get_template("config.html")
349426
config_html = config_tmpl.render(**config_ctx)
@@ -378,6 +455,7 @@ def main() -> None:
378455

379456
# HN usernames: from HN_USERNAME env var and/or local config file (gitignored)
380457
hn_usernames = hackernews.load_usernames(os.environ.get("HN_USERNAME") or None)
458+
feed_sources = _parse_feed_sources(os.environ.get("FEED_SOURCES") or None)
381459

382460
generate_site(
383461
repo=repo,
@@ -386,6 +464,7 @@ def main() -> None:
386464
youtube_playlist_ids=youtube_playlist_ids,
387465
youtube_channel_ids=youtube_channel_ids,
388466
hn_usernames=hn_usernames or None,
467+
feed_sources=feed_sources,
389468
)
390469

391470

blog/templates/config.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ <h2 class="config-section__heading">📡 Content Sources</h2>
106106
<thead>
107107
<tr>
108108
<th>Source</th>
109-
<th>Section</th>
109+
<th>Destination</th>
110110
<th>Status</th>
111111
<th>Posts</th>
112112
</tr>
@@ -119,7 +119,7 @@ <h2 class="config-section__heading">📡 Content Sources</h2>
119119
{{ repo_url }}/issues
120120
</a>
121121
</td>
122-
<td>My Writing</td>
122+
<td>{% if 'writing' in feed_sources %}<span class="config-status config-status--ok">Feed</span>{% else %}<span class="config-status config-status--off">Sidebar</span>{% endif %}</td>
123123
<td><span class="config-status config-status--ok">✅ Always active</span></td>
124124
<td>{{ writing_post_count }}</td>
125125
</tr>
@@ -145,7 +145,7 @@ <h2 class="config-section__heading">📡 Content Sources</h2>
145145
{% endif %}
146146
{% endif %}
147147
</td>
148-
<td>My Videos</td>
148+
<td>{% if 'channel' in feed_sources %}<span class="config-status config-status--ok">Feed</span>{% else %}<span class="config-status config-status--off">Sidebar</span>{% endif %}</td>
149149
<td>
150150
{% if channel_ids %}
151151
<span class="config-status config-status--ok">✅ Configured</span>
@@ -166,7 +166,7 @@ <h2 class="config-section__heading">📡 Content Sources</h2>
166166
<span class="config-detail">(via <code>YOUTUBE_PLAYLIST_IDS</code>)</span>
167167
{% endif %}
168168
</td>
169-
<td>My Watching <span class="config-detail">(sidebar)</span></td>
169+
<td>{% if 'playlists' in feed_sources %}<span class="config-status config-status--ok">Feed</span>{% else %}<span class="config-status config-status--off">Sidebar</span>{% endif %}</td>
170170
<td>
171171
{% if playlist_ids %}
172172
<span class="config-status config-status--ok">✅ Configured</span>
@@ -195,7 +195,7 @@ <h2 class="config-section__heading">📡 Content Sources</h2>
195195
</span>
196196
{% endif %}
197197
</td>
198-
<td>My Reading</td>
198+
<td>{% if 'hn_stories' in feed_sources or 'hn_comments' in feed_sources %}<span class="config-status config-status--ok">{% if 'hn_stories' in feed_sources and 'hn_comments' in feed_sources %}Feed (stories + comments){% elif 'hn_stories' in feed_sources %}Feed (stories) / Sidebar (comments){% else %}Feed (comments) / Sidebar (stories){% endif %}</span>{% else %}<span class="config-status config-status--off">Sidebar</span>{% endif %}</td>
199199
<td>
200200
{% if hn_usernames %}
201201
<span class="config-status config-status--ok">✅ Configured</span>

blog/templates/index.html

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -113,46 +113,50 @@ <h2 class="labels-sidebar__heading">Browse by label</h2>
113113

114114
</div>{# /main-column #}
115115

116-
{# ====== SIDEBAR — HN Comments + YouTube Playlists ====== #}
117-
{% if sidebar.hn_comments or sidebar.playlist_groups %}
116+
{# ====== SIDEBAR — generic panel loop ====== #}
117+
{% if sidebar_panels %}
118118
<aside class="page-sidebar">
119119

120-
{# --- Hacker News Comments --- #}
121-
{% if sidebar.hn_comments %}
122-
<section class="sidebar-section" id="sidebar-hn-comments">
120+
{% for panel in sidebar_panels %}
121+
<section class="sidebar-section" id="sidebar-{{ panel.type }}-{{ loop.index }}">
123122
<h2 class="sidebar-section__heading">
124-
<span aria-hidden="true">💬</span> HN Comments
123+
<span aria-hidden="true">{{ panel.icon }}</span> {{ panel.title }}
125124
</h2>
125+
126+
{# --- HN Comments --- #}
127+
{% if panel.type == 'hn_comments' %}
126128
<ul class="sidebar-list">
127-
{% for post in sidebar.hn_comments %}
129+
{% for post in panel.posts %}
128130
<li class="sidebar-item">
129131
<a class="sidebar-item__title" href="{{ post.metadata.story_url }}" target="_blank" rel="noopener noreferrer nofollow">{{ post.metadata.story_title }}</a>
130132
<div class="sidebar-item__meta">
131133
<a href="{{ post.metadata.hn_url }}" target="_blank" rel="noopener noreferrer nofollow">My comment</a>
132134
&middot; <time datetime="{{ post.created_at_iso }}">{{ post.created_at_fmt }}</time>
133135
</div>
134-
{% if post.excerpt %}
135-
<p class="sidebar-item__excerpt">{{ post.excerpt }}</p>
136-
{% endif %}
136+
{% if post.excerpt %}<p class="sidebar-item__excerpt">{{ post.excerpt }}</p>{% endif %}
137137
</li>
138138
{% endfor %}
139139
</ul>
140-
{% if sidebar.hn_threads_url %}
141-
<a class="sidebar-section__view-all" href="{{ sidebar.hn_threads_url }}" target="_blank" rel="noopener noreferrer nofollow">
142-
View all comments on HN &rarr;
143-
</a>
144-
{% endif %}
145-
</section>
146-
{% endif %}
147140

148-
{# --- YouTube Playlists (one panel per playlist) --- #}
149-
{% for grp in sidebar.playlist_groups %}
150-
<section class="sidebar-section" id="sidebar-playlist-{{ loop.index }}">
151-
<h2 class="sidebar-section__heading">
152-
<span aria-hidden="true">📺</span> My Watching
153-
</h2>
141+
{# --- HN Submissions --- #}
142+
{% elif panel.type == 'hn_stories' %}
143+
<ul class="sidebar-list">
144+
{% for post in panel.posts %}
145+
<li class="sidebar-item">
146+
<a class="sidebar-item__title" href="{{ post.metadata.article_url }}" target="_blank" rel="noopener noreferrer nofollow">{{ post.title }}</a>
147+
<div class="sidebar-item__meta">
148+
<time datetime="{{ post.created_at_iso }}">{{ post.created_at_fmt }}</time>
149+
&middot; {{ post.metadata.points }} pt{% if post.metadata.points != 1 %}s{% endif %}
150+
&middot; <a href="{{ post.metadata.hn_url }}" target="_blank" rel="noopener noreferrer nofollow">{{ post.metadata.num_comments }} comment{% if post.metadata.num_comments != 1 %}s{% endif %}</a>
151+
</div>
152+
</li>
153+
{% endfor %}
154+
</ul>
155+
156+
{# --- Video sources (playlist, channel) --- #}
157+
{% elif panel.type in ('playlist', 'channel') %}
154158
<ul class="sidebar-list sidebar-list--videos">
155-
{% for post in grp.posts %}
159+
{% for post in panel.posts %}
156160
<li class="sidebar-item sidebar-item--video">
157161
{% if post.avatar_url %}
158162
<a class="sidebar-item__thumb-link" href="{{ post.source_url }}" target="_blank" rel="noopener noreferrer nofollow">
@@ -161,16 +165,27 @@ <h2 class="sidebar-section__heading">
161165
{% endif %}
162166
<div class="sidebar-item__video-info">
163167
<a class="sidebar-item__title" href="{{ post.post_url }}">{{ post.title }}</a>
164-
<div class="sidebar-item__meta">
165-
<time datetime="{{ post.created_at_iso }}">{{ post.created_at_fmt }}</time>
166-
</div>
168+
<div class="sidebar-item__meta"><time datetime="{{ post.created_at_iso }}">{{ post.created_at_fmt }}</time></div>
167169
</div>
168170
</li>
169171
{% endfor %}
170172
</ul>
171-
{% if grp.view_more_url %}
172-
<a class="sidebar-section__view-all" href="{{ grp.view_more_url }}" target="_blank" rel="noopener noreferrer nofollow">
173-
View full playlist on YouTube &rarr;
173+
174+
{# --- Generic (writing, etc.) --- #}
175+
{% else %}
176+
<ul class="sidebar-list">
177+
{% for post in panel.posts %}
178+
<li class="sidebar-item">
179+
<a class="sidebar-item__title" href="{{ post.post_url }}">{{ post.title }}</a>
180+
<div class="sidebar-item__meta"><time datetime="{{ post.created_at_iso }}">{{ post.created_at_fmt }}</time></div>
181+
</li>
182+
{% endfor %}
183+
</ul>
184+
{% endif %}
185+
186+
{% if panel.view_all_url %}
187+
<a class="sidebar-section__view-all" href="{{ panel.view_all_url }}" target="_blank" rel="noopener noreferrer nofollow">
188+
View all &rarr;
174189
</a>
175190
{% endif %}
176191
</section>

0 commit comments

Comments
 (0)