brand: standardize "Upbeat Bytes" → "upbeatBytes" everywhere

Per the logo + brand: the name is upbeatBytes (camelCase). Swept all user-facing
strings — titles/og:site_name/og:title, logo alt text, share pages (share.py),
emails (email_send), classifier prompt (llm), digest/unsubscribe (api), PWA
manifest, game share text, sign-in, the SPA shell + patch-static-heads (play
title) — plus README/publish.sh and the email test fixture. (SMTP From env was
already upbeatBytes.) Domains (upbeatbytes.com) unchanged. 425 BE + 36 FE green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-28 20:01:20 -04:00
parent f8628b3b14
commit 667b1a82c3
24 changed files with 58 additions and 58 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Upbeat Bytes # upbeatBytes
Calm, constructive news — local-first ingestion, scoring, and a daily brief. Calm, constructive news — local-first ingestion, scoring, and a daily brief.
(The Python package and CLI are named `goodnews` for historical reasons; the (The Python package and CLI are named `goodnews` for historical reasons; the
product is **Upbeat Bytes**, at upbeatbytes.com.) product is **upbeatBytes**, at upbeatbytes.com.)
The first milestone is intentionally small: collect public RSS/Atom metadata, dedupe it, store short source-provided snippets, and attach early reason-coded heuristic scores. It does not store full article bodies. The first milestone is intentionally small: collect public RSS/Atom metadata, dedupe it, store short source-provided snippets, and attach early reason-coded heuristic scores. It does not store full article bodies.
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Publish Upbeat Bytes: build the frontend, sync it to the live Caddy site, # Publish upbeatBytes: build the frontend, sync it to the live Caddy site,
# rebuild/restart the API container, and reload Caddy. One command to redeploy. # rebuild/restart the API container, and reload Caddy. One command to redeploy.
set -euo pipefail set -euo pipefail
@@ -26,4 +26,4 @@ curl -fsS -o /dev/null -w ' logo-email.png → %{http_code} %{content_type}\n'
https://upbeatbytes.com/logo-email.png \ https://upbeatbytes.com/logo-email.png \
|| echo " ⚠ logo-email.png is not being served — the digest masthead would break!" || echo " ⚠ logo-email.png is not being served — the digest masthead would break!"
echo "✓ Published Upbeat Bytes → https://upbeatbytes.com" echo "✓ Published upbeatBytes → https://upbeatbytes.com"
+1 -1
View File
@@ -19,7 +19,7 @@ const PAGES = [
}, },
{ {
file: 'play.html', path: '/play', file: 'play.html', path: '/play',
title: 'Play · Upbeat Bytes — calm daily games', title: 'Play · upbeatBytes — calm daily games',
desc: 'A calm set of daily games — Daily Word, Word Search, Bloom, and Memory Match. ' + desc: 'A calm set of daily games — Daily Word, Word Search, Bloom, and Memory Match. ' +
'A friendly little break from the doomscroll.', 'A friendly little break from the doomscroll.',
}, },
+1 -1
View File
@@ -8,7 +8,7 @@
font-display: swap; font-display: swap;
} }
/* Upbeat Bytes calm design system. /* upbeatBytes calm design system.
Sand, sea, and sun: warm paper surfaces, a vivid-azure accent, gold highlight, Sand, sea, and sun: warm paper surfaces, a vivid-azure accent, gold highlight,
a serif voice for headlines, strong readable contrast, generous space. a serif voice for headlines, strong readable contrast, generous space.
No urgency colors (no red). Built around the logo's #0083ad azure. */ No urgency colors (no red). Built around the logo's #0083ad azure. */
+3 -3
View File
@@ -10,7 +10,7 @@
<meta name="description" content="A calmer, brighter corner of the internet: good news, daily art, small games, and little resets." /> <meta name="description" content="A calmer, brighter corner of the internet: good news, daily art, small games, and little resets." />
<title>upbeatBytes — a calmer, brighter corner of the internet</title> <title>upbeatBytes — a calmer, brighter corner of the internet</title>
<link rel="canonical" href="https://upbeatbytes.com/" /> <link rel="canonical" href="https://upbeatbytes.com/" />
<meta property="og:site_name" content="Upbeat Bytes" /> <meta property="og:site_name" content="upbeatBytes" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="upbeatBytes — a calmer, brighter corner of the internet" /> <meta property="og:title" content="upbeatBytes — a calmer, brighter corner of the internet" />
<meta property="og:description" content="A calmer, brighter corner of the internet: good news, daily art, small games, and little resets. No ads, no paywalls, no doomscrolling." /> <meta property="og:description" content="A calmer, brighter corner of the internet: good news, daily art, small games, and little resets. No ads, no paywalls, no doomscrolling." />
@@ -128,9 +128,9 @@
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
<div id="boot-fallback" role="alert" aria-live="polite"> <div id="boot-fallback" role="alert" aria-live="polite">
<div class="bf"> <div class="bf">
<img src="%sveltekit.assets%/logo.svg" alt="Upbeat Bytes" /> <img src="%sveltekit.assets%/logo.svg" alt="upbeatBytes" />
<p>We had a little trouble loading. A quick refresh usually sorts it out.</p> <p>We had a little trouble loading. A quick refresh usually sorts it out.</p>
<button type="button" onclick="location.reload()">Refresh Upbeat Bytes</button> <button type="button" onclick="location.reload()">Refresh upbeatBytes</button>
</div> </div>
</div> </div>
</body> </body>
@@ -59,7 +59,7 @@
if (value) onaction?.(kind, value); if (value) onaction?.(kind, value);
} }
// Sharing: share the branded Upbeat Bytes card page (default), with copy-source. // Sharing: share the branded upbeatBytes card page (default), with copy-source.
let shareOpen = $state(false); let shareOpen = $state(false);
let copied = $state(''); let copied = $state('');
const canNativeShare = typeof navigator !== 'undefined' && !!navigator.share; const canNativeShare = typeof navigator !== 'undefined' && !!navigator.share;
@@ -105,7 +105,7 @@
</a> </a>
{:else if usePlaceholder} {:else if usePlaceholder}
<a class="media placeholder" href={summaryHref} onclick={opened} style="--c:{accentColor}" tabindex="-1" aria-hidden="true"> <a class="media placeholder" href={summaryHref} onclick={opened} style="--c:{accentColor}" tabindex="-1" aria-hidden="true">
<span class="ph-word">{humanize(article.topic) || 'upbeat bytes'}</span> <span class="ph-word">{humanize(article.topic) || 'upbeatBytes'}</span>
</a> </a>
{/if} {/if}
+1 -1
View File
@@ -228,7 +228,7 @@
const breakdown = Object.keys(byLen).sort((a, b) => b - a).map((l) => `${l}×${byLen[l]}`).join(' '); const breakdown = Object.keys(byLen).sort((a, b) => b - a).map((l) => `${l}×${byLen[l]}`).join(' ');
const pang = found.some(isPangram) ? ' · pangram ✓' : ''; const pang = found.some(isPangram) ? ' · pangram ✓' : '';
const bloomV = mode === 'daily' ? 'daily' : (format === 'wild' ? 'free-wild' : 'free-center'); const bloomV = mode === 'daily' ? 'daily' : (format === 'wild' ? 'free-wild' : 'free-center');
const text = `Upbeat Bytes · Bloom ${date}\n${fullBloom ? 'Full Bloom 🌸' : tier.name} · ${found.length} words${pang}\n${breakdown}\n${gameShareUrl('bloom', bloomV)}`; const text = `upbeatBytes · Bloom ${date}\n${fullBloom ? 'Full Bloom 🌸' : tier.name} · ${found.length} words${pang}\n${breakdown}\n${gameShareUrl('bloom', bloomV)}`;
if (navigator.share) navigator.share({ text }).then(() => trackGame('bloom', 'shared')).catch(() => {}); if (navigator.share) navigator.share({ text }).then(() => trackGame('bloom', 'shared')).catch(() => {});
else navigator.clipboard?.writeText(text).then(() => { trackGame('bloom', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); }); else navigator.clipboard?.writeText(text).then(() => { trackGame('bloom', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); });
} }
+2 -2
View File
@@ -6,8 +6,8 @@
<header class="appbar"> <header class="appbar">
<div class="container bar"> <div class="container bar">
<a class="brand" href="/" aria-label="Upbeat Bytes — home"> <a class="brand" href="/" aria-label="upbeatBytes — home">
<img class="logo" src="/logo.svg" alt="Upbeat Bytes" width="586" height="196" /> <img class="logo" src="/logo.svg" alt="upbeatBytes" width="586" height="196" />
</a> </a>
<nav class="utils" aria-label="Your controls"> <nav class="utils" aria-label="Your controls">
+1 -1
View File
@@ -133,7 +133,7 @@
function share() { function share() {
const label = `${TIER_LABEL[tier] || tier} · ${format === 'colors' ? 'colors' : 'icons'}`; const label = `${TIER_LABEL[tier] || tier} · ${format === 'colors' ? 'colors' : 'icons'}`;
const when = isFree ? 'Free play' : date; const when = isFree ? 'Free play' : date;
const text = `Upbeat Bytes · Memory Match (${label}) ${when}\nCleared in ${moves} moves\n${gameShareUrl('match', `${mode}-${format}-${tier}`)}`; const text = `upbeatBytes · Memory Match (${label}) ${when}\nCleared in ${moves} moves\n${gameShareUrl('match', `${mode}-${format}-${tier}`)}`;
if (navigator.share) navigator.share({ text }).then(() => trackGame('match', 'shared')).catch(() => {}); if (navigator.share) navigator.share({ text }).then(() => trackGame('match', 'shared')).catch(() => {});
else navigator.clipboard?.writeText(text).then(() => { trackGame('match', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); }); else navigator.clipboard?.writeText(text).then(() => { trackGame('match', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); });
} }
+4 -4
View File
@@ -181,7 +181,7 @@
); );
let viewSubtitle = $derived( let viewSubtitle = $derived(
selected === 'today' ? localDateLabel(brief) selected === 'today' ? localDateLabel(brief)
: selected === 'search' ? 'Results across Upbeat Bytes' : selected === 'search' ? 'Results across upbeatBytes'
: selected.startsWith('source:') ? 'Latest from this source' : selected.startsWith('source:') ? 'Latest from this source'
: selected === 'latest' ? 'Freshest calm reads — newest first' : selected === 'latest' ? 'Freshest calm reads — newest first'
: selected === 'following' ? 'From the sources & topics you follow' : selected === 'following' ? 'From the sources & topics you follow'
@@ -629,7 +629,7 @@
moods = m; topics = c.topics; moods = m; topics = c.topics;
await loadView(selected); await loadView(selected);
} catch (e) { } catch (e) {
if (!brief) error = 'Could not reach Upbeat Bytes.'; // keep painted content if a refresh failed if (!brief) error = 'Could not reach upbeatBytes.'; // keep painted content if a refresh failed
} finally { } finally {
loading = false; loading = false;
} }
@@ -872,7 +872,7 @@
{#if families.length} {#if families.length}
<section id="explore" class="explore"> <section id="explore" class="explore">
<h2>Explore Upbeat Bytes</h2> <h2>Explore upbeatBytes</h2>
<div class="families"> <div class="families">
{#each families as f (f.name)} {#each families as f (f.name)}
{@const tags = f.tags.filter((t) => t.count > 0)} {@const tags = f.tags.filter((t) => t.count > 0)}
@@ -895,7 +895,7 @@
{#if !pwa.isStandalone && !pwa.dismissed && (pwa.canInstall || pwa.isIOS)} {#if !pwa.isStandalone && !pwa.dismissed && (pwa.canInstall || pwa.isIOS)}
<aside class="install rise"> <aside class="install rise">
<div class="install-text"> <div class="install-text">
<strong>Keep Upbeat Bytes a tap away.</strong> <strong>Keep upbeatBytes a tap away.</strong>
{#if pwa.canInstall}Add it to your home screen — it opens like an app, no store needed. {#if pwa.canInstall}Add it to your home screen — it opens like an app, no store needed.
{:else}On iPhone: tap the <span class="ios-share">Share</span> button, then “Add to Home Screen.”{/if} {:else}On iPhone: tap the <span class="ios-share">Share</span> button, then “Add to Home Screen.”{/if}
</div> </div>
+1 -1
View File
@@ -41,7 +41,7 @@
</p> </p>
<button class="primary" onclick={onclose}>Done</button> <button class="primary" onclick={onclose}>Done</button>
{:else} {:else}
<h2>Sign in to Upbeat Bytes</h2> <h2>Sign in to upbeatBytes</h2>
<p class="sub"> <p class="sub">
Save articles and keep your history across devices. Save articles and keep your history across devices.
</p> </p>
+1 -1
View File
@@ -167,7 +167,7 @@
const label = variant === '6' ? 'Long Word' : 'Daily Word'; const label = variant === '6' ? 'Long Word' : 'Daily Word';
const score = status === 'won' ? guesses.length : 'X'; const score = status === 'won' ? guesses.length : 'X';
const grid = cols.map((cs) => cs.map((c) => EMOJI[c]).join('')).join('\n'); const grid = cols.map((cs) => cs.map((c) => EMOJI[c]).join('')).join('\n');
const text = `Upbeat Bytes · ${label} ${date}\n${score}/${maxGuesses}\n${grid}\n${gameShareUrl('word', variant)}`; const text = `upbeatBytes · ${label} ${date}\n${score}/${maxGuesses}\n${grid}\n${gameShareUrl('word', variant)}`;
// Count a share only once it actually happens (sheet completed / clipboard wrote), // Count a share only once it actually happens (sheet completed / clipboard wrote),
// never on a cancelled share sheet or denied clipboard. // never on a cancelled share sheet or denied clipboard.
if (navigator.share) navigator.share({ text }).then(() => trackGame('word', 'shared')).catch(() => {}); if (navigator.share) navigator.share({ text }).then(() => trackGame('word', 'shared')).catch(() => {});
@@ -223,7 +223,7 @@
function share() { function share() {
const label = { small: 'Small', med: 'Medium', large: 'Large' }[size] || ''; const label = { small: 'Small', med: 'Medium', large: 'Large' }[size] || '';
const text = `Upbeat Bytes · Word Search (${label}) ${date}\n${theme} — cleared in ${fmt(resultMs)}\n${gameShareUrl('wordsearch', size)}`; const text = `upbeatBytes · Word Search (${label}) ${date}\n${theme} — cleared in ${fmt(resultMs)}\n${gameShareUrl('wordsearch', size)}`;
if (navigator.share) navigator.share({ text }).then(() => trackGame('wordsearch', 'shared')).catch(() => {}); if (navigator.share) navigator.share({ text }).then(() => trackGame('wordsearch', 'shared')).catch(() => {});
else navigator.clipboard?.writeText(text).then(() => { trackGame('wordsearch', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); }); else navigator.clipboard?.writeText(text).then(() => { trackGame('wordsearch', 'shared'); copied = true; setTimeout(() => (copied = false), 1500); });
} }
+1 -1
View File
@@ -101,7 +101,7 @@
<header class="bar"> <header class="bar">
<div class="container inner"> <div class="container inner">
<a class="brand" href="/" aria-label="Upbeat Bytes — home"><img class="logo" src="/logo.svg" alt="Upbeat Bytes" /></a> <a class="brand" href="/" aria-label="upbeatBytes — home"><img class="logo" src="/logo.svg" alt="upbeatBytes" /></a>
<div class="baractions"> <div class="baractions">
<button class="fb" onclick={openFeedback} aria-label="Share feedback" title="Share feedback"> <button class="fb" onclick={openFeedback} aria-label="Share feedback" title="Share feedback">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M4 5h16v11H8l-4 3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" /></svg> <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M4 5h16v11H8l-4 3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" /></svg>
+1 -1
View File
@@ -675,7 +675,7 @@
<header class="bar"> <header class="bar">
<div class="container inner"> <div class="container inner">
<a class="brand" href="/"><img class="logo" src="/logo.svg" alt="Upbeat Bytes" /></a> <a class="brand" href="/"><img class="logo" src="/logo.svg" alt="upbeatBytes" /></a>
<a class="back" href="/account"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>Account</a> <a class="back" href="/account"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>Account</a>
</div> </div>
</header> </header>
+1 -1
View File
@@ -35,7 +35,7 @@
{:else} {:else}
<h1>Couldn't sign you in</h1> <h1>Couldn't sign you in</h1>
<p class="muted">{error}</p> <p class="muted">{error}</p>
<a class="back" href="/">← Back to Upbeat Bytes</a> <a class="back" href="/">← Back to upbeatBytes</a>
{/if} {/if}
</main> </main>
+1 -1
View File
@@ -273,7 +273,7 @@
<!-- Canonical/OG/description for /play are baked into the static play.html at build <!-- Canonical/OG/description for /play are baked into the static play.html at build
time (scripts/patch-static-heads.mjs) so non-JS social scrapers get them; we keep time (scripts/patch-static-heads.mjs) so non-JS social scrapers get them; we keep
only the browser-tab title + dev-gate noindex here to avoid duplicate tags. --> only the browser-tab title + dev-gate noindex here to avoid duplicate tags. -->
<title>Play · Upbeat Bytes — calm daily games</title> <title>Play · upbeatBytes — calm daily games</title>
{#if isDevGated(game)}<meta name="robots" content="noindex" />{/if} {#if isDevGated(game)}<meta name="robots" content="noindex" />{/if}
</svelte:head> </svelte:head>
+2 -2
View File
@@ -51,13 +51,13 @@
</script> </script>
<svelte:head> <svelte:head>
<title>The Zen Den · Upbeat Bytes</title> <title>The Zen Den · upbeatBytes</title>
{#if isDevGated('zen')}<meta name="robots" content="noindex" />{/if} {#if isDevGated('zen')}<meta name="robots" content="noindex" />{/if}
</svelte:head> </svelte:head>
<header class="bar"> <header class="bar">
<div class="container inner"> <div class="container inner">
<a class="brand" href="/"><img class="logo" src="/logo.svg" alt="Upbeat Bytes" /></a> <a class="brand" href="/"><img class="logo" src="/logo.svg" alt="upbeatBytes" /></a>
<a class="back" href="/play"> <a class="back" href="/play">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>Play <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>Play
</a> </a>
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "Upbeat Bytes", "name": "upbeatBytes",
"short_name": "Upbeat Bytes", "short_name": "upbeatBytes",
"description": "A calmer, brighter corner of the internet: good news, daily art, small games, and little resets.", "description": "A calmer, brighter corner of the internet: good news, daily art, small games, and little resets.",
"start_url": "/", "start_url": "/",
"scope": "/", "scope": "/",
+3 -3
View File
@@ -767,7 +767,7 @@ def create_app() -> FastAPI:
ok = _do_unsubscribe(u, t) ok = _do_unsubscribe(u, t)
msg = ( msg = (
"Youre unsubscribed from the daily digest. No hard feelings — " "Youre unsubscribed from the daily digest. No hard feelings — "
"Upbeat Bytes is always here when you want it." "upbeatBytes is always here when you want it."
if ok else if ok else
"That unsubscribe link looks invalid or expired. You can manage the " "That unsubscribe link looks invalid or expired. You can manage the "
"digest from your account settings." "digest from your account settings."
@@ -776,9 +776,9 @@ def create_app() -> FastAPI:
'<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' '<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
'<div style="max-width:520px;margin:12vh auto;padding:0 24px;text-align:center;' '<div style="max-width:520px;margin:12vh auto;padding:0 24px;text-align:center;'
'font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#16263a">' 'font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#16263a">'
'<h1 style="font-size:22px">Upbeat Bytes</h1>' '<h1 style="font-size:22px">upbeatBytes</h1>'
f'<p style="font-size:16px;line-height:1.5;color:#3b4754">{msg}</p>' f'<p style="font-size:16px;line-height:1.5;color:#3b4754">{msg}</p>'
'<p><a href="/" style="color:#0083ad;text-decoration:none">← Back to Upbeat Bytes</a></p></div>' '<p><a href="/" style="color:#0083ad;text-decoration:none">← Back to upbeatBytes</a></p></div>'
) )
return HTMLResponse(html) return HTMLResponse(html)
+9 -9
View File
@@ -59,7 +59,7 @@ def send_email(to: str, subject: str, text: str, html: str | None = None, reply_
def send_feedback(to: str, category: str, message: str, contact: str | None, who: str) -> None: def send_feedback(to: str, category: str, message: str, contact: str | None, who: str) -> None:
"""Notify the admin of new user feedback (plain text is plenty here).""" """Notify the admin of new user feedback (plain text is plenty here)."""
subject = f"Upbeat Bytes feedback · {category}" subject = f"upbeatBytes feedback · {category}"
reply = contact or "(none given)" reply = contact or "(none given)"
text = ( text = (
f"New feedback ({category})\n" f"New feedback ({category})\n"
@@ -74,14 +74,14 @@ def send_feedback_reply(to: str, reply_text: str, reply_html: str | None, origin
"""Reply to a reader's feedback from the admin inbox. Sends multipart """Reply to a reader's feedback from the admin inbox. Sends multipart
text/plain + text/html (the HTML is the pre-sanitized Markdown render). Quotes text/plain + text/html (the HTML is the pre-sanitized Markdown render). Quotes
their original note for context; exposes no analytics/account details.""" their original note for context; exposes no analytics/account details."""
subject = "Re: Your Upbeat Bytes feedback" subject = "Re: Your upbeatBytes feedback"
quoted = "\n".join("> " + line for line in (original_message or "").splitlines()) quoted = "\n".join("> " + line for line in (original_message or "").splitlines())
text = ( text = (
f"{reply_text}\n\n" f"{reply_text}\n\n"
"\n" "\n"
"In reply to your note to Upbeat Bytes:\n" "In reply to your note to upbeatBytes:\n"
f"{quoted}\n\n" f"{quoted}\n\n"
"Thanks for reaching out.\nUpbeat Bytes\n" "Thanks for reaching out.\nupbeatBytes\n"
) )
body_html = None body_html = None
if reply_html: if reply_html:
@@ -90,9 +90,9 @@ def send_feedback_reply(to: str, reply_text: str, reply_html: str | None, origin
'<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;' '<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;'
'color:#16263a;font-size:15px;line-height:1.5">' 'color:#16263a;font-size:15px;line-height:1.5">'
f"{reply_html}" f"{reply_html}"
'<p style="color:#5d6b78;font-size:13px;margin-top:20px">In reply to your note to Upbeat Bytes:</p>' '<p style="color:#5d6b78;font-size:13px;margin-top:20px">In reply to your note to upbeatBytes:</p>'
f'<blockquote style="color:#5d6b78;border-left:3px solid #e8e3d8;margin:0;padding-left:12px">{oq}</blockquote>' f'<blockquote style="color:#5d6b78;border-left:3px solid #e8e3d8;margin:0;padding-left:12px">{oq}</blockquote>'
"<p>Thanks for reaching out.<br>— Upbeat Bytes</p></div>" "<p>Thanks for reaching out.<br>— upbeatBytes</p></div>"
) )
# Route the reader's reply to our chosen inbox (never back to the reader). # Route the reader's reply to our chosen inbox (never back to the reader).
cfg = _cfg() cfg = _cfg()
@@ -102,9 +102,9 @@ def send_feedback_reply(to: str, reply_text: str, reply_html: str | None, origin
def send_magic_link(to: str, link: str) -> None: def send_magic_link(to: str, link: str) -> None:
"""Send a calm, single-purpose sign-in email.""" """Send a calm, single-purpose sign-in email."""
subject = "Your Upbeat Bytes sign-in link" subject = "Your upbeatBytes sign-in link"
text = ( text = (
"Welcome back to Upbeat Bytes.\n\n" "Welcome back to upbeatBytes.\n\n"
f"Tap to sign in:\n{link}\n\n" f"Tap to sign in:\n{link}\n\n"
"This link works once and expires in 15 minutes.\n" "This link works once and expires in 15 minutes.\n"
"If you didn't request it, you can safely ignore this email." "If you didn't request it, you can safely ignore this email."
@@ -113,7 +113,7 @@ def send_magic_link(to: str, link: str) -> None:
html = ( html = (
'<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;' '<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;'
'color:#16263a;line-height:1.6">' 'color:#16263a;line-height:1.6">'
"<p>Welcome back to <strong>Upbeat Bytes</strong>.</p>" "<p>Welcome back to <strong>upbeatBytes</strong>.</p>"
f'<p><a href="{safe}" style="display:inline-block;background:#0083ad;color:#fff;' f'<p><a href="{safe}" style="display:inline-block;background:#0083ad;color:#fff;'
'text-decoration:none;padding:11px 20px;border-radius:999px;font-weight:600">' 'text-decoration:none;padding:11px 20px;border-radius:999px;font-weight:600">'
"Sign in</a></p>" "Sign in</a></p>"
+1 -1
View File
@@ -78,7 +78,7 @@ _RESPONSE_FORMATS = (
) )
SYSTEM_PROMPT = """You classify article metadata for Upbeat Bytes, a calm news digest. SYSTEM_PROMPT = """You classify article metadata for upbeatBytes, a calm news digest.
The bar is NOT "is this happy?" it is "will a reader finish this calm or a little better, never worse?" ACCEPT stories that are calm, neutral, insightful, or uplifting: they inform, teach, delight, or show progress or benefit. Neutral-but-absorbing is welcome a discovery, a clear explainer, a clever build or gadget, a fascinating bit of science, space, nature, design, or culture, a genuinely useful insight even when it isn't "feel-good." The bar is NOT "is this happy?" it is "will a reader finish this calm or a little better, never worse?" ACCEPT stories that are calm, neutral, insightful, or uplifting: they inform, teach, delight, or show progress or benefit. Neutral-but-absorbing is welcome a discovery, a clear explainer, a clever build or gadget, a fascinating bit of science, space, nature, design, or culture, a genuinely useful insight even when it isn't "feel-good."
+13 -13
View File
@@ -20,7 +20,7 @@ def _tag(name: str, content: str | None, attr: str = "property") -> str:
def render_share_page(article: dict, base_url: str, summary: str | None = None, def render_share_page(article: dict, base_url: str, summary: str | None = None,
explanation: dict | None = None) -> str: explanation: dict | None = None) -> str:
aid = article["id"] aid = article["id"]
title = (article.get("title") or "Upbeat Bytes").strip() title = (article.get("title") or "upbeatBytes").strip()
why = (article.get("reason_text") or article.get("description") why = (article.get("reason_text") or article.get("description")
or "A calm, constructive story worth your attention.").strip() or "A calm, constructive story worth your attention.").strip()
source = (article.get("source_name") or "the source").strip() source = (article.get("source_name") or "the source").strip()
@@ -39,7 +39,7 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None,
twitter_card = "summary_large_image" if image else "summary" twitter_card = "summary_large_image" if image else "summary"
meta = "\n".join(filter(None, [ meta = "\n".join(filter(None, [
_tag("og:site_name", "Upbeat Bytes"), _tag("og:site_name", "upbeatBytes"),
_tag("og:type", "article"), _tag("og:type", "article"),
_tag("og:title", title), _tag("og:title", title),
_tag("og:description", why), _tag("og:description", why),
@@ -138,7 +138,7 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None,
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{escape(title)} · Upbeat Bytes</title> <title>{escape(title)} · upbeatBytes</title>
<meta name="description" content="{escape(why)}"> <meta name="description" content="{escape(why)}">
<link rel="canonical" href="{escape(page_url)}"> <link rel="canonical" href="{escape(page_url)}">
<link rel="icon" href="/favicon.svg"> <link rel="icon" href="/favicon.svg">
@@ -194,7 +194,7 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None,
</style> </style>
</head> </head>
<body> <body>
<div class="bar"><div class="inner"><a href="/"><img src="/logo.svg" alt="Upbeat Bytes"></a><button class="back" type="button" data-back aria-label="Go back"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>Back</button></div></div> <div class="bar"><div class="inner"><a href="/"><img src="/logo.svg" alt="upbeatBytes"></a><button class="back" type="button" data-back aria-label="Go back"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>Back</button></div></div>
<main class="wrap"> <main class="wrap">
<article class="card"> <article class="card">
{media} {media}
@@ -207,9 +207,9 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None,
<div class="actions"> <div class="actions">
<a class="primary" href="{escape(src_url)}" target="_blank" rel="noopener" data-src-click>Read the full story at {escape(source)}</a> <a class="primary" href="{escape(src_url)}" target="_blank" rel="noopener" data-src-click>Read the full story at {escape(source)}</a>
<button class="secondary" type="button" data-share>Copy link</button> <button class="secondary" type="button" data-share>Copy link</button>
<a class="secondary" href="/news">Explore Upbeat Bytes </a> <a class="secondary" href="/news">Explore upbeatBytes </a>
</div> </div>
<p class="note">Upbeat Bytes summarizes in its own words and links to the original publisher it doesn't host the article.</p> <p class="note">upbeatBytes summarizes in its own words and links to the original publisher it doesn't host the article.</p>
</div> </div>
</article> </article>
</main> </main>
@@ -275,7 +275,7 @@ def render_digest(items: list[dict], base_url: str, brief_date: str | None) -> s
) )
meta = "\n".join(filter(None, [ meta = "\n".join(filter(None, [
_tag("og:site_name", "Upbeat Bytes"), _tag("og:site_name", "upbeatBytes"),
_tag("og:type", "website"), _tag("og:type", "website"),
_tag("og:title", "Today's good news, summarized"), _tag("og:title", "Today's good news, summarized"),
_tag("og:description", intro), _tag("og:description", intro),
@@ -292,7 +292,7 @@ def render_digest(items: list[dict], base_url: str, brief_date: str | None) -> s
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Today's good news, summarized · Upbeat Bytes</title> <title>Today's good news, summarized · upbeatBytes</title>
<meta name="description" content="{escape(intro)}"> <meta name="description" content="{escape(intro)}">
<link rel="canonical" href="{page_url}"> <link rel="canonical" href="{page_url}">
<link rel="icon" href="/favicon.svg"> <link rel="icon" href="/favicon.svg">
@@ -324,12 +324,12 @@ def render_digest(items: list[dict], base_url: str, brief_date: str | None) -> s
</style> </style>
</head> </head>
<body> <body>
<div class="bar"><div class="inner"><a href="/"><img src="/logo.svg" alt="Upbeat Bytes"></a></div></div> <div class="bar"><div class="inner"><a href="/"><img src="/logo.svg" alt="upbeatBytes"></a></div></div>
<main class="wrap"> <main class="wrap">
<h1>Today's good news</h1> <h1>Today's good news</h1>
<p class="lede">{escape(intro)}{f' · {escape(brief_date)}' if brief_date else ''}</p> <p class="lede">{escape(intro)}{f' · {escape(brief_date)}' if brief_date else ''}</p>
{cards} {cards}
<p class="more"><a href="/news">Browse more on Upbeat Bytes </a></p> <p class="more"><a href="/news">Browse more on upbeatBytes </a></p>
</main> </main>
</body> </body>
</html>""" </html>"""
@@ -339,7 +339,7 @@ def render_not_found(base_url: str) -> str:
return f"""<!doctype html> return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"> <html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Story not found · Upbeat Bytes</title><link rel="icon" href="/favicon.svg"> <title>Story not found · upbeatBytes</title><link rel="icon" href="/favicon.svg">
<style> <style>
body {{ margin:0; min-height:100vh; display:flex; flex-direction:column; align-items:center; body {{ margin:0; min-height:100vh; display:flex; flex-direction:column; align-items:center;
justify-content:center; gap:14px; text-align:center; padding:40px; justify-content:center; gap:14px; text-align:center; padding:40px;
@@ -348,8 +348,8 @@ def render_not_found(base_url: str) -> str:
a {{ color:#006b8e; }} a {{ color:#006b8e; }}
</style></head> </style></head>
<body> <body>
<img src="/logo.svg" alt="Upbeat Bytes" style="height:44px"> <img src="/logo.svg" alt="upbeatBytes" style="height:44px">
<h1 style="font-family:Georgia,serif;font-weight:600">That story isn't here</h1> <h1 style="font-family:Georgia,serif;font-weight:600">That story isn't here</h1>
<p style="color:#5d6b78">It may have moved on the good news refreshes often.</p> <p style="color:#5d6b78">It may have moved on the good news refreshes often.</p>
<a href="/"> Back to Upbeat Bytes</a> <a href="/"> Back to upbeatBytes</a>
</body></html>""" </body></html>"""
+2 -2
View File
@@ -43,6 +43,6 @@ def test_reply_to_uses_env_inbox(monkeypatch):
def test_reply_to_falls_back_to_from(monkeypatch): def test_reply_to_falls_back_to_from(monkeypatch):
_arm(monkeypatch) _arm(monkeypatch)
monkeypatch.delenv("GOODNEWS_REPLY_TO_EMAIL", raising=False) monkeypatch.delenv("GOODNEWS_REPLY_TO_EMAIL", raising=False)
monkeypatch.setenv("GOODNEWS_SMTP_FROM", "Upbeat Bytes <hello@upbeatbytes.com>") monkeypatch.setenv("GOODNEWS_SMTP_FROM", "upbeatBytes <hello@upbeatbytes.com>")
es.send_feedback_reply("reader@x.com", "hi", None, "o") es.send_feedback_reply("reader@x.com", "hi", None, "o")
assert _FakeSMTP.last["Reply-To"] == "Upbeat Bytes <hello@upbeatbytes.com>" assert _FakeSMTP.last["Reply-To"] == "upbeatBytes <hello@upbeatbytes.com>"