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.write_text(html, encoding="utf-8")
|
||||||
index_tmp.rename(index)
|
index_tmp.rename(index)
|
||||||
log(f"wrote {index} (and archive {archive.name})")
|
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 ─────────────────────────────────────────────────────────────
|
# ── 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">
|
<div class="masthead-edition">
|
||||||
<span class="edition-num">№ {{ generated_at.strftime("%j") }}</span>
|
<span class="edition-num">№ {{ generated_at.strftime("%j") }}</span>
|
||||||
<span class="edition-stamp">{{ edition|upper }} EDITION</span>
|
<span class="edition-stamp">{{ edition|upper }} EDITION</span>
|
||||||
|
<a class="archive-link" href="archive.html">ARCHIVE →</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="masthead-meta">
|
<div class="masthead-meta">
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
@@ -92,19 +93,24 @@
|
|||||||
<p class="item-tldr">{{ it.tldr }}</p>
|
<p class="item-tldr">{{ it.tldr }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<footer class="item-foot">
|
<footer class="item-foot">
|
||||||
<a class="item-link" href="{{ it.permalink }}" target="_blank" rel="noopener">
|
<div class="item-actions">
|
||||||
comments
|
{% if it.url and it.url != it.permalink %}
|
||||||
</a>
|
<a class="action action-source" href="{{ it.url }}" target="_blank" rel="noopener">
|
||||||
{% if it.url and it.url != it.permalink %}
|
<span class="action-arrow">↗</span>
|
||||||
<span class="sep">·</span>
|
<span class="action-label">SOURCE</span>
|
||||||
<a class="item-link" href="{{ it.url }}" target="_blank" rel="noopener">
|
<span class="action-domain">{{ it.url|domain }}</span>
|
||||||
{{ it.url|domain }}
|
</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>
|
</a>
|
||||||
{% endif %}
|
</div>
|
||||||
<span class="sep">·</span>
|
<div class="item-meta-row">
|
||||||
<span class="item-meta">{{ it.posted_at|humanago }} ago</span>
|
<span class="item-meta">{{ it.posted_at|humanago }} ago</span>
|
||||||
<span class="sep">·</span>
|
<span class="sep">·</span>
|
||||||
<span class="item-meta">u/{{ it.author }}</span>
|
<span class="item-meta">u/{{ it.author }}</span>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -150,16 +156,21 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<footer class="item-foot">
|
<footer class="item-foot">
|
||||||
{% if it.url %}
|
{% if it.url %}
|
||||||
<a class="item-link" href="{{ it.url }}" target="_blank" rel="noopener">
|
<div class="item-actions">
|
||||||
{{ it.url|domain }}
|
<a class="action action-source" href="{{ it.url }}" target="_blank" rel="noopener">
|
||||||
</a>
|
<span class="action-arrow">↗</span>
|
||||||
<span class="sep">·</span>
|
<span class="action-label">SOURCE</span>
|
||||||
{% endif %}
|
<span class="action-domain">{{ it.url|domain }}</span>
|
||||||
<span class="item-meta">{{ it.posted_at|humanago }} ago</span>
|
</a>
|
||||||
{% if it.author %}
|
</div>
|
||||||
<span class="sep">·</span>
|
|
||||||
<span class="item-meta">{{ it.author }}</span>
|
|
||||||
{% endif %}
|
{% 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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -131,6 +131,14 @@ a:hover { color: var(--accent); }
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
font-family: var(--font-mono);
|
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 {
|
.edition-num {
|
||||||
color: var(--fg-faint);
|
color: var(--fg-faint);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -400,26 +408,132 @@ a:hover { color: var(--accent); }
|
|||||||
.item-foot {
|
.item-foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 16px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
letter-spacing: .06em;
|
letter-spacing: .06em;
|
||||||
color: var(--fg-faint);
|
color: var(--fg-faint);
|
||||||
}
|
}
|
||||||
.item-link {
|
/* Action chips — explicit "go here" buttons. Reddit items get TWO of
|
||||||
color: var(--fg-dim);
|
these when the post links externally: SOURCE (the linked content)
|
||||||
border-bottom: 1px dotted var(--fg-faint);
|
and REDDIT THREAD (the discussion). Non-Reddit items get just SOURCE. */
|
||||||
padding-bottom: 1px;
|
.item-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.item-link:hover { color: var(--accent); border-color: var(--accent-dim); }
|
.action {
|
||||||
.item-meta { font-variant-numeric: tabular-nums; }
|
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); }
|
.sep { color: var(--rule-bright); }
|
||||||
|
|
||||||
/* Visual de-emphasize purely meme/skip items if any slip through. */
|
/* Visual de-emphasize purely meme/skip items if any slip through. */
|
||||||
.item[data-tag="meme"] { opacity: .58; }
|
.item[data-tag="meme"] { opacity: .58; }
|
||||||
.item[data-tag="other"] { opacity: .85; }
|
.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
|
empty state + colophon
|
||||||
------------------------------------------------------------------------- */
|
------------------------------------------------------------------------- */
|
||||||
|
|||||||
Reference in New Issue
Block a user