mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat: rename project to kill-the-news, add GitHub Pages landing page
- Rename all references from Email-to-RSS/email-to-rss to kill-the-news across README.md, AGENTS.md, package.json, wrangler-example.toml, setup.sh - Add docs/index.html: dark-themed landing page for GitHub Pages covering features, how it works, quick start, and tech stack Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ This file gives coding agents fast context for working in this repository.
|
|||||||
|
|
||||||
## Project summary
|
## Project summary
|
||||||
|
|
||||||
Email-to-RSS is a Cloudflare Worker that ingests newsletters from ForwardEmail and exposes them as RSS feeds.
|
kill-the-news is a Cloudflare Worker that ingests email newsletters and exposes them as private RSS feeds.
|
||||||
|
|
||||||
Core goals:
|
Core goals:
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Email-to-RSS
|
# kill-the-news
|
||||||
|
|
||||||
Convert email newsletters into a private RSS feed using Cloudflare Workers.
|
Convert email newsletters into private RSS feeds using Cloudflare Workers.
|
||||||
|
|
||||||
This project is self-hosted, uses your own domain, and keeps your data in your own Cloudflare account.
|
Self-hosted, uses your own domain, and keeps your data in your own Cloudflare account. Live at [kill-the.news](https://kill-the.news).
|
||||||
|
|
||||||
## Why this exists
|
## Why this exists
|
||||||
|
|
||||||
Many newsletters only support email delivery. RSS readers offer a better reading experience, but getting email-only newsletters into RSS usually means relying on shared third-party infrastructure.
|
Many newsletters only support email delivery. RSS readers offer a better reading experience, but getting email-only newsletters into RSS usually means relying on shared third-party infrastructure.
|
||||||
|
|
||||||
Email-to-RSS keeps the same workflow while avoiding shared domains and shared data stores.
|
kill-the-news keeps the same workflow while avoiding shared domains and shared data stores.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ No third-party service required. Cloudflare receives the email and hands it dire
|
|||||||
1. In the Cloudflare dashboard, go to _Email → Email Routing_ for your zone and click **Enable Email Routing**. Cloudflare will prompt you to add MX and SPF records — accept and it adds them automatically.
|
1. In the Cloudflare dashboard, go to _Email → Email Routing_ for your zone and click **Enable Email Routing**. Cloudflare will prompt you to add MX and SPF records — accept and it adds them automatically.
|
||||||
2. Under _Email Routing → Routing Rules_, add a **Catch-all** rule:
|
2. Under _Email Routing → Routing Rules_, add a **Catch-all** rule:
|
||||||
- Action: **Send to Worker**
|
- Action: **Send to Worker**
|
||||||
- Worker: `email-to-rss` (the name from `wrangler.toml`)
|
- Worker: `kill-the-news` (the name from `wrangler.toml`)
|
||||||
|
|
||||||
That's it. No webhook configuration is needed.
|
That's it. No webhook configuration is needed.
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ The Worker verifies each webhook request against ForwardEmail's published MX IP
|
|||||||
|
|
||||||
6. Open `https://yourdomain.com/admin` and sign in.
|
6. Open `https://yourdomain.com/admin` and sign in.
|
||||||
|
|
||||||
> **Tip:** To verify the Worker is running, check _Workers & Pages → email-to-rss_ in the Cloudflare dashboard. The _Custom Domains_ tab should list your domain once the deploy succeeds.
|
> **Tip:** To verify the Worker is running, check _Workers & Pages → kill-the-news_ in the Cloudflare dashboard. The _Custom Domains_ tab should list your domain once the deploy succeeds.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+595
@@ -0,0 +1,595 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>kill-the-news — Private newsletter feeds on Cloudflare Workers</title>
|
||||||
|
<meta name="description" content="Convert email newsletters into private RSS feeds using Cloudflare Workers. Self-hosted, free tier, your own domain." />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a0a;
|
||||||
|
--surface: #111111;
|
||||||
|
--border: #222222;
|
||||||
|
--text: #f0f0f0;
|
||||||
|
--muted: #888888;
|
||||||
|
--accent: #f6821f;
|
||||||
|
--accent-dim: rgba(246,130,31,0.12);
|
||||||
|
--radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: inherit; text-decoration: none; }
|
||||||
|
|
||||||
|
/* ── Nav ── */
|
||||||
|
nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 2rem;
|
||||||
|
height: 60px;
|
||||||
|
background: rgba(10,10,10,0.85);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo svg { color: var(--accent); }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.45rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.btn-outline:hover { background: var(--surface); }
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { opacity: 0.88; }
|
||||||
|
|
||||||
|
/* ── Hero ── */
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 6rem 2rem 5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(ellipse 80% 60% at 50% -10%, rgba(246,130,31,0.18) 0%, transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(246,130,31,0.35);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: clamp(2.2rem, 5vw, 3.5rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 span { color: var(--accent); }
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 0 auto 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-ctas {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Section layout ── */
|
||||||
|
section {
|
||||||
|
padding: 5rem 2rem;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: clamp(1.6rem, 3vw, 2.2rem);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-sub {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header { margin-bottom: 3rem; }
|
||||||
|
|
||||||
|
/* ── Features ── */
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.feature-card:hover { border-color: rgba(246,130,31,0.4); }
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── How it works ── */
|
||||||
|
#how-it-works { background: var(--surface); max-width: none; padding: 5rem 2rem; }
|
||||||
|
#how-it-works .inner { max-width: 1100px; margin: 0 auto; }
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 0;
|
||||||
|
margin-top: 3rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 2rem 2rem 2rem 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 2.75rem;
|
||||||
|
width: 1px;
|
||||||
|
height: calc(100% - 5.5rem);
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Quick Start ── */
|
||||||
|
#quick-start {}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-header span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.dot-r { background: #ff5f57; }
|
||||||
|
.dot-y { background: #febc2e; }
|
||||||
|
.dot-g { background: #28c840; }
|
||||||
|
|
||||||
|
.code-block pre {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block pre .prompt { color: var(--muted); user-select: none; }
|
||||||
|
.code-block pre .cmd { color: var(--text); }
|
||||||
|
.code-block pre .comment { color: #555; }
|
||||||
|
|
||||||
|
.ingestion-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingestion-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingestion-card.recommended { border-color: rgba(246,130,31,0.4); }
|
||||||
|
|
||||||
|
.ingestion-card .tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.tag-recommended { background: var(--accent-dim); color: var(--accent); }
|
||||||
|
.tag-alt { background: rgba(255,255,255,0.06); color: var(--muted); }
|
||||||
|
|
||||||
|
.ingestion-card h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 0.4rem; }
|
||||||
|
.ingestion-card p { font-size: 0.85rem; color: var(--muted); line-height: 1.6; }
|
||||||
|
|
||||||
|
/* ── Tech stack ── */
|
||||||
|
.tech-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.65rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Footer ── */
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a { color: var(--muted); border-bottom: 1px solid var(--border); transition: color 0.15s; }
|
||||||
|
footer a:hover { color: var(--text); }
|
||||||
|
footer .sep { margin: 0 0.5rem; }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.step:not(:last-child)::after { display: none; }
|
||||||
|
.step { padding-right: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Nav -->
|
||||||
|
<nav>
|
||||||
|
<div class="nav-logo">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||||
|
<polyline points="22,6 12,13 2,6"/>
|
||||||
|
</svg>
|
||||||
|
kill-the-news
|
||||||
|
</div>
|
||||||
|
<a href="https://github.com/juherr/kill-the-news" class="btn btn-outline" target="_blank" rel="noopener">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.2 11.38.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.61-4.04-1.61-.54-1.38-1.33-1.75-1.33-1.75-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.8 1.3 3.49 1 .11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.12-3.17 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 3-.4c1.02.005 2.04.14 3 .4 2.28-1.55 3.29-1.23 3.29-1.23.66 1.65.24 2.87.12 3.17.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.83.58C20.57 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Hero -->
|
||||||
|
<div class="hero">
|
||||||
|
<div class="hero-glow"></div>
|
||||||
|
<div class="badge">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10"/></svg>
|
||||||
|
Open Source · MIT · Free Tier
|
||||||
|
</div>
|
||||||
|
<h1>Turn email newsletters into <span>private RSS feeds</span></h1>
|
||||||
|
<p>Self-hosted on Cloudflare Workers. Your data stays in your own account, served from your own domain.</p>
|
||||||
|
<div class="hero-ctas">
|
||||||
|
<a href="https://github.com/juherr/kill-the-news" class="btn btn-primary" target="_blank" rel="noopener">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.2 11.38.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.61-4.04-1.61-.54-1.38-1.33-1.75-1.33-1.75-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.8 1.3 3.49 1 .11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.12-3.17 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 3-.4c1.02.005 2.04.14 3 .4 2.28-1.55 3.29-1.23 3.29-1.23.66 1.65.24 2.87.12 3.17.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.83.58C20.57 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||||
|
View on GitHub
|
||||||
|
</a>
|
||||||
|
<a href="#quick-start" class="btn btn-outline">Quick Start ↓</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Features -->
|
||||||
|
<section id="features">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-label">Features</div>
|
||||||
|
<h2 class="section-title">Everything you need, nothing you don't</h2>
|
||||||
|
<p class="section-sub">Built on serverless infrastructure — zero servers to maintain, no subscription fees.</p>
|
||||||
|
</div>
|
||||||
|
<div class="features-grid">
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>Self-Hosted & Private</h3>
|
||||||
|
<p>Your emails and feeds live exclusively in your own Cloudflare account. No shared infrastructure, no data mining.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>Free Cloudflare Tier</h3>
|
||||||
|
<p>Cloudflare Workers, KV, and Email Routing all fall within the generous free tier. Deploy at zero cost.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>Your Own Domain</h3>
|
||||||
|
<p>Subscribe to newsletters using addresses on your own domain (e.g. <code style="font-family:monospace;font-size:0.8em;color:var(--accent)">apple@yourdomain.com</code>). No lock-in.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>Two Ingestion Methods</h3>
|
||||||
|
<p>Use Cloudflare Email Routing (no third-party) or ForwardEmail webhooks — whichever fits your setup.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>Attachment Enclosures</h3>
|
||||||
|
<p>Email attachments are stored in Cloudflare R2 and exposed as RSS enclosures — no extra hosting needed.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>External Auth Support</h3>
|
||||||
|
<p>Optionally delegate admin authentication to Authelia, Authentik, or any reverse proxy that sets <code style="font-family:monospace;font-size:0.8em;color:var(--accent)">Remote-User</code>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- How it works -->
|
||||||
|
<div id="how-it-works">
|
||||||
|
<div class="inner">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-label">How it works</div>
|
||||||
|
<h2 class="section-title">Three steps, done</h2>
|
||||||
|
<p class="section-sub">From email delivery to your RSS reader in milliseconds, with no moving parts.</p>
|
||||||
|
</div>
|
||||||
|
<div class="steps">
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">1</div>
|
||||||
|
<h3>Subscribe with your address</h3>
|
||||||
|
<p>Create a feed in the admin UI and get a unique address like <code style="font-family:monospace;font-size:0.8em;color:var(--accent)">newsletter.42@yourdomain.com</code>. Subscribe to any newsletter with it.</p>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">2</div>
|
||||||
|
<h3>Worker ingests the email</h3>
|
||||||
|
<p>When a newsletter arrives, Cloudflare routes it to your Worker. It parses the content and stores it in KV — attachments go to R2.</p>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<h3>Read in your RSS reader</h3>
|
||||||
|
<p>Your feed is live at <code style="font-family:monospace;font-size:0.8em;color:var(--accent)">/rss/:feedId</code>. Add it to any RSS client and never miss an issue.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Start -->
|
||||||
|
<section id="quick-start">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-label">Quick Start</div>
|
||||||
|
<h2 class="section-title">Up and running in minutes</h2>
|
||||||
|
<p class="section-sub">A single setup script handles KV namespaces, secrets, and <code style="font-family:monospace;font-size:0.9em">wrangler.toml</code> generation.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-block">
|
||||||
|
<div class="code-block-header">
|
||||||
|
<span class="dot-r"></span><span class="dot-y"></span><span class="dot-g"></span>
|
||||||
|
Terminal
|
||||||
|
</div>
|
||||||
|
<pre><span class="comment"># 1. Clone the repo</span>
|
||||||
|
<span class="prompt">$ </span><span class="cmd">git clone https://github.com/juherr/kill-the-news.git && cd kill-the-news</span>
|
||||||
|
|
||||||
|
<span class="comment"># 2. Log in to Cloudflare</span>
|
||||||
|
<span class="prompt">$ </span><span class="cmd">npx wrangler login</span>
|
||||||
|
|
||||||
|
<span class="comment"># 3. Run the interactive setup (creates KV, sets secrets, writes wrangler.toml)</span>
|
||||||
|
<span class="prompt">$ </span><span class="cmd">bash setup.sh</span>
|
||||||
|
|
||||||
|
<span class="comment"># 4. Deploy to the edge</span>
|
||||||
|
<span class="prompt">$ </span><span class="cmd">npm run deploy</span></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:2rem;">
|
||||||
|
<p style="font-size:0.9rem;color:var(--muted);margin-bottom:1rem;">Then choose how emails reach your Worker:</p>
|
||||||
|
<div class="ingestion-options">
|
||||||
|
<div class="ingestion-card recommended">
|
||||||
|
<div class="tag tag-recommended">Recommended</div>
|
||||||
|
<h3>Cloudflare Email Routing</h3>
|
||||||
|
<p>Enable Email Routing on your domain and add a catch-all rule that forwards to the Worker. No third-party service required.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ingestion-card">
|
||||||
|
<div class="tag tag-alt">Alternative</div>
|
||||||
|
<h3>ForwardEmail Webhook</h3>
|
||||||
|
<p>Point ForwardEmail MX records at your domain. ForwardEmail parses incoming mail and POSTs a JSON payload to <code style="font-family:monospace;font-size:0.8em">/api/inbound</code>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tech Stack -->
|
||||||
|
<section id="tech-stack" style="padding-top:0;">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-label">Tech Stack</div>
|
||||||
|
<h2 class="section-title">Built on reliable primitives</h2>
|
||||||
|
<p class="section-sub">Minimal dependencies, maximum portability — runs entirely on Cloudflare's global network.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tech-pills">
|
||||||
|
<span class="pill">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="color:var(--accent)"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
||||||
|
Cloudflare Workers
|
||||||
|
</span>
|
||||||
|
<span class="pill">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent)"><rect x="2" y="2" width="9" height="9"/><rect x="13" y="2" width="9" height="9"/><rect x="13" y="13" width="9" height="9"/><rect x="2" y="13" width="9" height="9"/></svg>
|
||||||
|
Hono
|
||||||
|
</span>
|
||||||
|
<span class="pill">KV Storage</span>
|
||||||
|
<span class="pill">R2 Object Storage</span>
|
||||||
|
<span class="pill">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="color:#3178c6"><path d="M0 12v12h24V0H0zm19.341-.956c.61.152 1.074.423 1.501.865.221.236.549.666.575.769.008.03-1.036.73-1.668 1.123-.023.015-.115-.084-.217-.236-.31-.45-.633-.644-1.128-.678-.728-.05-1.196.331-1.192.967a.88.88 0 0 0 .102.45c.16.331.458.53 1.39.933 1.719.74 2.454 1.227 2.911 1.92.51.773.625 2.008.278 2.926-.38.998-1.325 1.676-2.655 1.9-.411.073-1.386.062-1.828-.018-.964-.172-1.878-.648-2.442-1.273-.221-.243-.652-.88-.625-.925.011-.016.11-.077.22-.141.108-.061.511-.294.892-.515l.69-.4.145.214c.202.308.643.731.91.872.766.404 1.817.347 2.335-.118a.883.883 0 0 0 .313-.72c0-.278-.035-.4-.18-.61-.186-.266-.567-.49-1.649-.96-1.238-.533-1.771-.864-2.259-1.39a3.165 3.165 0 0 1-.659-1.2c-.091-.339-.114-1.189-.042-1.531.255-1.197 1.158-2.03 2.461-2.278.423-.08 1.406-.05 1.821.053z"/></svg>
|
||||||
|
TypeScript
|
||||||
|
</span>
|
||||||
|
<span class="pill">RSS 2.0</span>
|
||||||
|
<span class="pill">Atom</span>
|
||||||
|
<span class="pill">postal-mime</span>
|
||||||
|
<span class="pill">Zod</span>
|
||||||
|
<span class="pill">Vitest</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/juherr/kill-the-news" target="_blank" rel="noopener">kill-the-news</a>
|
||||||
|
<span class="sep">·</span>
|
||||||
|
MIT License
|
||||||
|
<span class="sep">·</span>
|
||||||
|
Built on <a href="https://workers.cloudflare.com/" target="_blank" rel="noopener">Cloudflare Workers</a>
|
||||||
|
<span class="sep">·</span>
|
||||||
|
Inspired by <a href="https://github.com/leafac/kill-the-newsletter" target="_blank" rel="noopener">kill-the-newsletter</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "email-to-rss",
|
"name": "kill-the-news",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "A service that converts email newsletters to RSS feeds using Cloudflare Workers",
|
"description": "Convert email newsletters into private RSS feeds using Cloudflare Workers",
|
||||||
"main": "dist/worker.js",
|
"main": "dist/worker.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "wrangler deploy --dry-run --outdir=dist",
|
"build": "wrangler deploy --dry-run --outdir=dist",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "🚀 Setting up Email to RSS service..."
|
echo "🚀 Setting up kill-the-news..."
|
||||||
|
|
||||||
if ! command -v npm >/dev/null 2>&1 || ! command -v npx >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1; then
|
if ! command -v npm >/dev/null 2>&1 || ! command -v npx >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1; then
|
||||||
echo "❌ Error: Node.js (with npm and npx) is required but not found."
|
echo "❌ Error: Node.js (with npm and npx) is required but not found."
|
||||||
@@ -17,7 +17,7 @@ fi
|
|||||||
|
|
||||||
WORKER_NAME="$(grep -E '^name = "' wrangler-example.toml | head -1 | cut -d'"' -f2)"
|
WORKER_NAME="$(grep -E '^name = "' wrangler-example.toml | head -1 | cut -d'"' -f2)"
|
||||||
if [ -z "$WORKER_NAME" ]; then
|
if [ -z "$WORKER_NAME" ]; then
|
||||||
WORKER_NAME="email-to-rss"
|
WORKER_NAME="kill-the-news"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "📦 Installing dependencies..."
|
echo "📦 Installing dependencies..."
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name = "email-to-rss"
|
name = "kill-the-news"
|
||||||
main = "src/index.ts"
|
main = "src/index.ts"
|
||||||
compatibility_date = "REPLACE_WITH_COMPATIBILITY_DATE"
|
compatibility_date = "REPLACE_WITH_COMPATIBILITY_DATE"
|
||||||
compatibility_flags = ["nodejs_compat"]
|
compatibility_flags = ["nodejs_compat"]
|
||||||
|
|||||||
Reference in New Issue
Block a user