news-digest: separated SOURCE / REDDIT THREAD action chips + archive page
Two requested polish items:
1. Reddit items now show TWO distinct action chips in the footer:
[↗ SOURCE huggingface.co] [⌥ REDDIT THREAD] 3h · u/foo
Previously the external link was a tiny dot-separated text link
that visually competed with the post metadata. Now: explicit
labeled buttons, distinct colors (cyan for source, blue for
thread), with hover states that match the Australis accent
palette. Non-Reddit items get just the SOURCE chip.
2. Archive page at /archive.html — lists every edition-*.html on
disk, newest-first, sorted PM-before-AM within a day. Each row
is the date in editorial serif + an AM/PM chip color-coded
(yellow morning / cyan evening) + a hover-affordance arrow.
Header link from the main edition reads "ARCHIVE →".
Generation: digest.py walks OUTPUT_DIR for the edition-*.html
filename pattern on every run, sorts, renders archive.html.j2,
writes atomically. Cheap (~1ms even with hundreds of editions).
No retention cap — twice-daily for a year is ~700 small files,
well under any reasonable concern.
CSS additions: .action chip styles (with per-action color variants),
.archive-link in masthead, full .archive-row + .archive-edition
treatment.
This commit is contained in:
@@ -353,6 +353,54 @@ def write_output(html: str, generated_at: datetime) -> None:
|
||||
index_tmp.write_text(html, encoding="utf-8")
|
||||
index_tmp.rename(index)
|
||||
log(f"wrote {index} (and archive {archive.name})")
|
||||
write_archive_index(generated_at)
|
||||
|
||||
ARCHIVE_FNAME_RE = re.compile(r"^edition-(\d{4}-\d{2}-\d{2})-(am|pm)\.html$")
|
||||
|
||||
def write_archive_index(generated_at: datetime) -> None:
|
||||
"""Render /output/archive.html — list every edition-*.html in
|
||||
OUTPUT_DIR, newest-first. Cheap (re-runs every digest fire);
|
||||
template loads from the same TEMPLATE_DIR."""
|
||||
editions = []
|
||||
for p in OUTPUT_DIR.glob("edition-*.html"):
|
||||
m = ARCHIVE_FNAME_RE.match(p.name)
|
||||
if not m:
|
||||
continue
|
||||
date_str, ed = m.group(1), m.group(2)
|
||||
try:
|
||||
date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
continue
|
||||
editions.append({
|
||||
"filename": p.name,
|
||||
"date": date,
|
||||
"edition": "morning" if ed == "am" else "evening",
|
||||
"edition_short": ed.upper(),
|
||||
"date_long": date.strftime("%A %B %-d, %Y"),
|
||||
# Sort key: date descending, then PM before AM (within a day,
|
||||
# PM is the most recent edition).
|
||||
"_sort": (date, 1 if ed == "pm" else 0),
|
||||
})
|
||||
editions.sort(key=lambda e: e["_sort"], reverse=True)
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(str(TEMPLATE_DIR)),
|
||||
autoescape=select_autoescape(["html"]),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
tpl = env.get_template("archive.html.j2")
|
||||
html = tpl.render(
|
||||
editions=editions,
|
||||
generated_at=generated_at,
|
||||
total=len(editions),
|
||||
)
|
||||
|
||||
out = OUTPUT_DIR / "archive.html"
|
||||
out_tmp = out.with_suffix(".html.tmp")
|
||||
out_tmp.write_text(html, encoding="utf-8")
|
||||
out_tmp.rename(out)
|
||||
log(f"wrote {out} ({len(editions)} editions indexed)")
|
||||
|
||||
# ── main ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Daily Digest · Archive</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="masthead" role="banner">
|
||||
<div class="masthead-inner">
|
||||
<div class="masthead-brand">
|
||||
<span class="brand-glyph" aria-hidden="true">◢</span>
|
||||
<span class="brand-mark">DAILY DIGEST</span>
|
||||
</div>
|
||||
<div class="masthead-edition">
|
||||
<span class="edition-stamp">ARCHIVE</span>
|
||||
<a class="archive-link" href="index.html">← LATEST</a>
|
||||
</div>
|
||||
<div class="masthead-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">EDITIONS ON FILE</span>
|
||||
<span class="meta-value">{{ total }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">CADENCE</span>
|
||||
<span class="meta-value">2× DAILY · 0800 / 2000</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">RETENTION</span>
|
||||
<span class="meta-value">UNLIMITED</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aurora-rule" aria-hidden="true"></div>
|
||||
</header>
|
||||
|
||||
<main class="brief">
|
||||
<section class="desk">
|
||||
<header class="desk-head">
|
||||
<span class="desk-num">∞</span>
|
||||
<h2 class="desk-title">All Editions</h2>
|
||||
<span class="desk-sub">newest first</span>
|
||||
</header>
|
||||
|
||||
{% if editions %}
|
||||
<ol class="archive-list">
|
||||
{% for e in editions %}
|
||||
<li class="archive-item">
|
||||
<a class="archive-row" href="{{ e.filename }}">
|
||||
<span class="archive-date">{{ e.date_long }}</span>
|
||||
<span class="archive-edition archive-edition-{{ e.edition_short|lower }}">
|
||||
{{ e.edition_short }}
|
||||
</span>
|
||||
<span class="archive-arrow">→</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% else %}
|
||||
<p class="empty-msg">No editions on file yet. Cron fires twice daily at 0800 / 2000.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="colophon">
|
||||
<div class="colophon-inner">
|
||||
<div class="colophon-block">
|
||||
<span class="meta-label">INDEXED</span>
|
||||
<span class="meta-value">{{ generated_at.isoformat(timespec="seconds") }}</span>
|
||||
</div>
|
||||
<div class="colophon-block">
|
||||
<span class="meta-label">STORE</span>
|
||||
<span class="meta-value">/opt/docker/data/news-digest</span>
|
||||
</div>
|
||||
<div class="colophon-block">
|
||||
<span class="meta-label">FORMAT</span>
|
||||
<span class="meta-value">edition-YYYY-MM-DD-{am,pm}.html</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,6 +17,7 @@
|
||||
<div class="masthead-edition">
|
||||
<span class="edition-num">№ {{ generated_at.strftime("%j") }}</span>
|
||||
<span class="edition-stamp">{{ edition|upper }} EDITION</span>
|
||||
<a class="archive-link" href="archive.html">ARCHIVE →</a>
|
||||
</div>
|
||||
<div class="masthead-meta">
|
||||
<div class="meta-row">
|
||||
@@ -92,19 +93,24 @@
|
||||
<p class="item-tldr">{{ it.tldr }}</p>
|
||||
{% endif %}
|
||||
<footer class="item-foot">
|
||||
<a class="item-link" href="{{ it.permalink }}" target="_blank" rel="noopener">
|
||||
comments
|
||||
</a>
|
||||
{% if it.url and it.url != it.permalink %}
|
||||
<span class="sep">·</span>
|
||||
<a class="item-link" href="{{ it.url }}" target="_blank" rel="noopener">
|
||||
{{ it.url|domain }}
|
||||
<div class="item-actions">
|
||||
{% if it.url and it.url != it.permalink %}
|
||||
<a class="action action-source" href="{{ it.url }}" target="_blank" rel="noopener">
|
||||
<span class="action-arrow">↗</span>
|
||||
<span class="action-label">SOURCE</span>
|
||||
<span class="action-domain">{{ it.url|domain }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="action action-thread" href="{{ it.permalink }}" target="_blank" rel="noopener">
|
||||
<span class="action-arrow">⌥</span>
|
||||
<span class="action-label">REDDIT THREAD</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<span class="sep">·</span>
|
||||
<span class="item-meta">{{ it.posted_at|humanago }} ago</span>
|
||||
<span class="sep">·</span>
|
||||
<span class="item-meta">u/{{ it.author }}</span>
|
||||
</div>
|
||||
<div class="item-meta-row">
|
||||
<span class="item-meta">{{ it.posted_at|humanago }} ago</span>
|
||||
<span class="sep">·</span>
|
||||
<span class="item-meta">u/{{ it.author }}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</li>
|
||||
@@ -150,16 +156,21 @@
|
||||
{% endif %}
|
||||
<footer class="item-foot">
|
||||
{% if it.url %}
|
||||
<a class="item-link" href="{{ it.url }}" target="_blank" rel="noopener">
|
||||
{{ it.url|domain }}
|
||||
</a>
|
||||
<span class="sep">·</span>
|
||||
{% endif %}
|
||||
<span class="item-meta">{{ it.posted_at|humanago }} ago</span>
|
||||
{% if it.author %}
|
||||
<span class="sep">·</span>
|
||||
<span class="item-meta">{{ it.author }}</span>
|
||||
<div class="item-actions">
|
||||
<a class="action action-source" href="{{ it.url }}" target="_blank" rel="noopener">
|
||||
<span class="action-arrow">↗</span>
|
||||
<span class="action-label">SOURCE</span>
|
||||
<span class="action-domain">{{ it.url|domain }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="item-meta-row">
|
||||
<span class="item-meta">{{ it.posted_at|humanago }} ago</span>
|
||||
{% if it.author %}
|
||||
<span class="sep">·</span>
|
||||
<span class="item-meta">{{ it.author }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -131,6 +131,14 @@ a:hover { color: var(--accent); }
|
||||
gap: 18px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.archive-link {
|
||||
color: var(--fg-dim);
|
||||
font-size: 11px;
|
||||
letter-spacing: .14em;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.archive-link:hover { color: var(--accent); border-color: var(--accent-dim); }
|
||||
.edition-num {
|
||||
color: var(--fg-faint);
|
||||
font-size: 11px;
|
||||
@@ -400,26 +408,132 @@ a:hover { color: var(--accent); }
|
||||
.item-foot {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: .06em;
|
||||
color: var(--fg-faint);
|
||||
}
|
||||
.item-link {
|
||||
color: var(--fg-dim);
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
padding-bottom: 1px;
|
||||
/* Action chips — explicit "go here" buttons. Reddit items get TWO of
|
||||
these when the post links externally: SOURCE (the linked content)
|
||||
and REDDIT THREAD (the discussion). Non-Reddit items get just SOURCE. */
|
||||
.item-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.item-link:hover { color: var(--accent); border-color: var(--accent-dim); }
|
||||
.item-meta { font-variant-numeric: tabular-nums; }
|
||||
.action {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--rule-bright);
|
||||
border-radius: 2px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fg-dim);
|
||||
transition: border-color 120ms, color 120ms, background 120ms;
|
||||
}
|
||||
.action-arrow {
|
||||
color: var(--accent-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
.action-label { color: var(--fg); }
|
||||
.action-domain {
|
||||
color: var(--fg-dim);
|
||||
font-weight: 500;
|
||||
letter-spacing: .04em;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.action:hover {
|
||||
border-color: var(--accent);
|
||||
background: rgba(66, 220, 209, 0.08);
|
||||
}
|
||||
.action:hover .action-arrow,
|
||||
.action:hover .action-label,
|
||||
.action:hover .action-domain { color: var(--accent); }
|
||||
|
||||
.action-source { /* default styling above is "source" */ }
|
||||
.action-thread .action-arrow { color: var(--aus-bright-blue); }
|
||||
.action-thread:hover { border-color: var(--aus-bright-blue); background: rgba(164, 196, 255, 0.08); }
|
||||
.action-thread:hover .action-arrow,
|
||||
.action-thread:hover .action-label { color: var(--aus-bright-blue); }
|
||||
|
||||
.item-meta-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-left: auto; /* pushes timestamp to the right */
|
||||
}
|
||||
.item-meta { font-variant-numeric: tabular-nums; color: var(--fg-faint); }
|
||||
.sep { color: var(--rule-bright); }
|
||||
|
||||
/* Visual de-emphasize purely meme/skip items if any slip through. */
|
||||
.item[data-tag="meme"] { opacity: .58; }
|
||||
.item[data-tag="other"] { opacity: .85; }
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
archive page — list of all editions
|
||||
------------------------------------------------------------------------- */
|
||||
|
||||
.archive-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.archive-item + .archive-item { border-top: 1px solid var(--rule); }
|
||||
|
||||
.archive-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 16px 20px;
|
||||
background: var(--surface);
|
||||
transition: background 120ms;
|
||||
}
|
||||
.archive-row:hover {
|
||||
background: var(--surface-hi);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.archive-date {
|
||||
font-family: var(--font-display);
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.005em;
|
||||
color: var(--fg);
|
||||
font-variation-settings: "opsz" 36;
|
||||
}
|
||||
|
||||
.archive-edition {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .14em;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.archive-edition-am { color: var(--aus-yellow); }
|
||||
.archive-edition-pm { color: var(--aus-bright-cyan); }
|
||||
|
||||
.archive-arrow {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--fg-faint);
|
||||
font-size: 14px;
|
||||
}
|
||||
.archive-row:hover .archive-arrow { color: var(--accent); }
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
empty state + colophon
|
||||
------------------------------------------------------------------------- */
|
||||
|
||||
Reference in New Issue
Block a user