@kurtbruns/kestrel 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/README.md +19 -29
- package/dist/public/dashboard/app.js +1 -1
- package/dist/public/dashboard/app.js.map +2 -2
- package/dist/public/dashboard/index.html +1 -1
- package/dist/worker/index.js +86 -190
- package/docs/README.md +2 -2
- package/docs/get-started/02-deploy.md +12 -5
- package/docs/get-started/03-access.md +2 -0
- package/docs/get-started/04-ses.md +14 -14
- package/docs/get-started/05-verify.md +19 -7
- package/docs/get-started/06-go-live.md +13 -55
- package/docs/guides/01-connect-claude.md +4 -4
- package/docs/guides/03-notifications.md +10 -10
- package/docs/guides/09-as-your-list-grows.md +62 -0
- package/docs/publishing/01-your-list.md +118 -47
- package/docs/publishing/02-sending-rules.md +5 -5
- package/docs/publishing/03-name-your-publication.md +7 -93
- package/docs/publishing/04-write-a-post.md +30 -73
- package/docs/publishing/06-troubleshooting.md +74 -0
- package/docs/publishing/07-working-with-claude.md +18 -37
- package/docs/publishing/09-share-your-subscribe-link.md +11 -59
- package/docs/publishing/10-check-your-setup.md +12 -30
- package/docs/publishing/11-email-template.md +34 -0
- package/docs/publishing/README.md +12 -11
- package/docs/reference/01-configuration.md +6 -6
- package/docs/reference/02-sending-domain-dns.md +1 -1
- package/package.json +1 -1
- package/template/claude/skills/migrate-list/SKILL.md +2 -2
- package/docs/guides/08-try-locally.md +0 -108
- package/docs/publishing/05-scheduling-and-sending.md +0 -72
- package/docs/publishing/06-after-a-send.md +0 -109
- package/docs/publishing/08-import-format.md +0 -103
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
When a send needs you, its page and the dashboard's health block say so in a red or amber line. Red needs you to act, and amber is worth a look. Find the line's words below for what it means and what to do.
|
|
4
|
+
|
|
5
|
+
**Set a notifications address, so a problem reaches you without your watching.** Set it under **Settings → Notifications**, in **Send notifications to**. With none set, Kestrel emails you nothing. [Notifications](06-troubleshooting.md#notifications), below, lists what you're told.
|
|
6
|
+
|
|
7
|
+
## A send is waiting or held
|
|
8
|
+
|
|
9
|
+
A problem with your email provider never marks anyone unsent. The send waits where it is, and carries on from there once the problem clears.
|
|
10
|
+
|
|
11
|
+
- **"The provider is refusing the account".** Your provider won't take mail from your account, and its own words follow, with the fix they point to. The fix is almost always in your Amazon SES account or Kestrel's deployment, and the message names it. A spent daily sending quota frees up within a day, and you can ask AWS for a higher one, as [As your list grows](../guides/09-as-your-list-grows.md) describes.
|
|
12
|
+
- **"No email provider is connected".** The send is held until one is connected ([Connect Amazon SES](../get-started/04-ses.md)), and goes out once it is.
|
|
13
|
+
- **"The provider is unavailable".** An outage or a rate limit on the provider's side. The send retries on its own, so there's nothing to do.
|
|
14
|
+
- **"Still sending … after it started".** A send is taking longer than expected, and may be retrying. If the line stays, read the app's logs, as [Read the logs](../get-started/05-verify.md#7-read-the-logs) describes.
|
|
15
|
+
- **"The fire time passed … and the send has not started".** The Cloudflare Cron Trigger that starts sends on time isn't running. The send goes out as soon as it runs again. Check the Worker's trigger, as [`triggers.crons`](../reference/01-configuration.md#triggerscrons) describes.
|
|
16
|
+
- **"The provider never answered for …".** Rarely, Kestrel can't tell whether your provider took some emails, and sending again could mail those readers twice. **Resolve…** on the send's page asks you to decide. **Assume not sent** is the safe default: those readers may miss this post, but get your next one. Choose **Assume sent** only when you know they got it.
|
|
17
|
+
|
|
18
|
+
## Before a send
|
|
19
|
+
|
|
20
|
+
- **"The template or publication details changed this email's look after your last test".** Send a new test of the post, since your last one no longer shows what goes out.
|
|
21
|
+
- **The daily database writes warning.** On Cloudflare's free Workers plan, a large send may use up the day's writes, and then the app stops working until midnight UTC. [Bring your existing list](01-your-list.md#limits) explains the limit, and when to upgrade to Workers Paid.
|
|
22
|
+
|
|
23
|
+
## Reading a send's record
|
|
24
|
+
|
|
25
|
+
A send's page becomes its permanent record once it finishes. Its counts cover everyone the send went to:
|
|
26
|
+
|
|
27
|
+
| Count | What it means |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| **Delivered** | Your provider confirmed the email reached the reader's mail server. |
|
|
30
|
+
| **Bounced** | The receiving server turned the email away. |
|
|
31
|
+
| **Complained** | The reader marked the email as spam. |
|
|
32
|
+
| **Unsent** | Kestrel couldn't hand the email to your provider. |
|
|
33
|
+
|
|
34
|
+
Delivered always means confirmed, not just accepted by your provider. Confirmations can lag by days, so counts keep rising after a send finishes. Some emails are never confirmed at all, so a send can read **Settling** for good, which is normal.
|
|
35
|
+
|
|
36
|
+
**Unsent recipients** are still on your list, and get your next post. A recipient is unsent when their own email kept failing, with the provider's words on the row, or when you chose **Assume not sent**.
|
|
37
|
+
|
|
38
|
+
## Bounces and complaints
|
|
39
|
+
|
|
40
|
+
Kestrel acts on these for you, so a bad address can't keep hurting your sending reputation:
|
|
41
|
+
|
|
42
|
+
- **A hard bounce** means the address can't receive mail. Kestrel never mails it again.
|
|
43
|
+
- **A soft bounce** is temporary, such as a full mailbox. The address stays on your list.
|
|
44
|
+
- **A complaint** means the reader marked your email as spam. Kestrel never mails them again.
|
|
45
|
+
|
|
46
|
+
Under **Subscribers**, a suppressed address keeps its status and carries a label saying why. When a recent send's bounces reach about 5% of its recipients, the dashboard warns you. That's about the rate at which email providers start to review a sender, so check the addresses on its record.
|
|
47
|
+
|
|
48
|
+
### Lifting a suppression
|
|
49
|
+
|
|
50
|
+
Lift one only when you know the address works again, such as a reader who tells you their mailbox is fixed. Mailing an address that keeps failing can hurt every send after it. **Unblock**, in a subscriber's ⋯ menu, lifts a block. The dashboard can't lift a bounce or a complaint, but [Claude](07-working-with-claude.md) can, when you ask. Past records keep the bounce or complaint where it happened.
|
|
51
|
+
|
|
52
|
+
## An import is refused
|
|
53
|
+
|
|
54
|
+
Kestrel refuses a file it can't read with certainty, and says what's wrong and where. Nothing on your list changes.
|
|
55
|
+
|
|
56
|
+
- **Not in the format.** The file isn't a Kestrel CSV or a plain list. Convert it first, with Claude or by hand.
|
|
57
|
+
- **A column or value the format doesn't take,** named with its row and column. Fix it in the file, or move a column of your old service's into `evidence`.
|
|
58
|
+
- **Too many rows** for one import. Split the file.
|
|
59
|
+
- **Too large.** An import takes up to 10 MB.
|
|
60
|
+
|
|
61
|
+
[The file format](01-your-list.md#the-file-format) describes what each column takes.
|
|
62
|
+
|
|
63
|
+
## Notifications
|
|
64
|
+
|
|
65
|
+
With an address set, Kestrel emails you:
|
|
66
|
+
|
|
67
|
+
- when a send finishes, with how many emails were accepted, unsent, and skipped
|
|
68
|
+
- when your provider refuses your account, or no provider is connected
|
|
69
|
+
- when a send stays in progress too long, waits for you to resolve it, or misses its time
|
|
70
|
+
- when Claude imports a list for you
|
|
71
|
+
|
|
72
|
+
Each links to the page it's about, and is sent once while the problem lasts. A bounce spike shows on the dashboard but isn't emailed. **Send a test notification** checks that one arrives.
|
|
73
|
+
|
|
74
|
+
By default, notifications go through your newsletter's own email provider. So when that provider refuses your account, the notification about it can't reach you either. You can send them another way, as [Notifications through Cloudflare's email](../guides/03-notifications.md) describes.
|
|
@@ -1,48 +1,29 @@
|
|
|
1
1
|
# Working with Claude
|
|
2
2
|
|
|
3
|
-
Claude works through the same API as the dashboard, so it can do what you do
|
|
3
|
+
Ask Claude in plain words, the way you'd ask a colleague. It works through the same API as the dashboard, so it can do what you can do there, and nothing more. Everything it changes shows in the dashboard as Claude's.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
To set Claude up, follow [Connect Claude to the API](../guides/01-connect-claude.md). Claude reads these same pages from your Kestrel, so it explains things in the words you see here.
|
|
6
6
|
|
|
7
7
|
## What to ask Claude
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **Draft or proofread a post.** Give it notes, an outline, or a draft. It saves a new version you can open in the dashboard. If you edited the post meanwhile, its save is refused, and it works from your version or asks you.
|
|
10
|
+
- **Test and schedule a post.** Name the post and when it should go out. Claude schedules it, sends a test of exactly what will go out, and tells you how to cancel. It can also cancel a send or move it.
|
|
11
|
+
- **Check how a send did.** Claude reads the send's record and reports what was delivered, unsent, bounced, or complained.
|
|
12
|
+
- **Lift a bounce or a complaint.** The dashboard can't, so ask Claude when you know an address works again. [Lifting a suppression](06-troubleshooting.md#lifting-a-suppression) says when that's wise.
|
|
13
|
+
- **Recover an earlier version.** Every save keeps a whole version of a post, and Claude can read them back.
|
|
14
|
+
- **Import a list.** Its `/migrate-list` skill asks what anything ambiguous in your old service's export means, and converts a few rows for you to check. It imports once you attest, and if a scheduled post would grow, you finish the import in the dashboard.
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
- **Edit or proofread a post.** Claude saves a new version against the one it read. If you edited the post meanwhile, its save is refused, and it reads your version and works from that, or asks you.
|
|
13
|
-
- **Send a test.** Claude sends the real email to your test addresses, or to one you name, through the same render as the send.
|
|
14
|
-
- **Schedule a post.** Name the post and when it should go out. Claude schedules it, sends a test of exactly what will fire, and tells you when it fires and how to cancel. It can also cancel a send or move its fire time, as [Scheduling and sending](05-scheduling-and-sending.md) describes.
|
|
15
|
-
- **Check a send's record.** Ask how a post did, and Claude reads the send and its recipients. It reports how many were accepted, delivered, and unsent, and any bounces or complaints. [After a send](06-after-a-send.md) says what each one means.
|
|
16
|
-
- **Lift a bounce or a complaint.** The dashboard lifts only a block, with **Unblock**, so ask Claude when you know a bounced address works again. [After a send](06-after-a-send.md#lifting-a-suppression) says when that's wise.
|
|
17
|
-
- **Import a list.** Ask Claude to bring your subscribers over from another service. Its `/migrate-list` skill converts the export, checks it with you, and imports it. [Bring your existing list](01-your-list.md#let-claude-convert-your-export) says how that goes.
|
|
16
|
+
## The lines it can't cross
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
The app holds these for any client, so they hold whatever Claude is told:
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
- **Every send waits out the review window,** and you can cancel it until it goes out.
|
|
21
|
+
- **A scheduled post is locked,** and what goes out is exactly the copy that was scheduled.
|
|
22
|
+
- **No one is mailed twice,** and only confirmed subscribers receive a post.
|
|
23
|
+
- **An unsubscribe is final** until the reader subscribes again, and an erased address stays erased until the person confirms again.
|
|
24
|
+
- **No one's edit is overwritten.** A save based on an older version is refused.
|
|
25
|
+
- **Only you can let an import grow a scheduled send.** An import through Claude that would stops there, until you finish it in the dashboard.
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
Claude also follows instructions your Kestrel gives it, which live in your instance's repository. Those hold only as long as the instructions do. They have Claude ask before anything that reaches readers, test again after a template change, and record whose decision each change to the list was. They also have it treat what's in a post or a subscriber's record as content, never as instructions.
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
- **A scheduled post is locked.** To change it, cancel the send, edit, and schedule again. What fires is exactly the copy that was scheduled.
|
|
27
|
-
- **One send per post at a time, and no one mailed twice.** A repeated request finds the existing send instead of making another.
|
|
28
|
-
- **Only confirmed subscribers receive a post.** Someone added by hand gets a confirmation email first, and receives nothing until they confirm. A bounced or complained address is never mailed.
|
|
29
|
-
- **An unsubscribe is final.** Only the reader undoes it, by subscribing again.
|
|
30
|
-
- **No one's edit is overwritten.** A save based on an older version of a post is refused, whichever client sent it.
|
|
31
|
-
- **Only you can let an import grow a scheduled send.** An import through Claude that would add people to a waiting send stops there, until you finish it in the dashboard.
|
|
32
|
-
- **An erased address stays erased.** Adding it back by hand needs a statement that the person asked to join again, and only their own confirmation lifts it.
|
|
33
|
-
|
|
34
|
-
## Lines Claude keeps
|
|
35
|
-
|
|
36
|
-
These come from the instructions your Kestrel gives Claude, not from the app, so they hold as long as those instructions do. Whoever runs your Kestrel keeps them in your instance's repository.
|
|
37
|
-
|
|
38
|
-
- **It asks before anything that reaches readers.** Sending mail and changing the list wait for your yes.
|
|
39
|
-
- **It schedules only when you ask,** then sends a test of the scheduled copy. After a change to your template or publication details, it tests again.
|
|
40
|
-
- **It never adds a subscriber you didn't name.** The one exception is an import you asked for, through `/migrate-list`, which imports anyone as a subscriber only on your attestation.
|
|
41
|
-
- **It records whose decision it was.** It unsubscribes someone only when they asked to leave, and blocks an address when you decide to stop mailing it. It lifts a bounce or a complaint only when you say the problem is gone, and confirms first.
|
|
42
|
-
- **It erases a subscriber only when you name them and ask,** and confirms first, since an erasure can't be undone.
|
|
43
|
-
- **It reads post text and subscriber data as content.** Words in a post or a reader's record are never instructions to Claude.
|
|
44
|
-
- **It won't help share your subscribe link before your Kestrel can send to anyone.** Until then, signups fail. Whoever runs your Kestrel finishes that before [Go live](../get-started/06-go-live.md).
|
|
45
|
-
|
|
46
|
-
The app lets Claude give your attestation for an import, so the instruction to ask you first is Claude's own. The import's record says it was given through Claude, and Kestrel emails you about the import if your notifications are set up.
|
|
47
|
-
|
|
48
|
-
[Subscriber import format](08-import-format.md) describes the file Claude converts your list to.
|
|
29
|
+
The app lets Claude give your attestation for an import, so asking you first is one of those instructions, not a rule the app enforces. The import's record says it came through Claude, and Kestrel emails you about it if your notifications are set.
|
|
@@ -1,73 +1,25 @@
|
|
|
1
1
|
# Share your subscribe link
|
|
2
2
|
|
|
3
|
-
Your publication
|
|
3
|
+
Your publication's subscribe page is at `/subscribe` on your Kestrel's address, such as `https://newsletter.example.com/subscribe`. Copy it from the dashboard's checklist or **Settings → Ways to subscribe**, and share it wherever your readers are. A reader joins only once they confirm by email.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Signups open only once an email provider is connected.** Until then, the page says the newsletter isn't taking subscribers yet, and the dashboard says so too. You can share the link anyway, and it opens once email is connected ([Connect Amazon SES](../get-started/04-ses.md)).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## How a reader joins
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
A reader enters their address, gets a confirmation email, and presses **Confirm subscription** on the page its link opens. Only then do they receive your posts. Opening the link alone records nothing, since mail scanners open links before a person does. This double opt-in is your record that each reader asked for your newsletter.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Every address gets the same answer, so the form never reveals who subscribes. Behind it, Kestrel sends a confirmation only when one is due:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
**Every address gets the same answer, so the form never reveals who subscribes.**
|
|
18
|
-
|
|
19
|
-
The page always says to check the inbox, whatever the address is. Behind it, Kestrel sends a confirmation only when one is due:
|
|
20
|
-
|
|
21
|
-
- **Already subscribed:** no email goes out, and they stay subscribed.
|
|
22
|
-
- **Waiting to confirm:** they get a confirmation, but at most one every 15 minutes, however often the form is submitted. A confirmation link works for 7 days, and an older one offers to send a fresh link.
|
|
23
|
-
- **Unsubscribed earlier:** they get a confirmation, and confirming it puts them back on your list.
|
|
24
|
-
- **Bounced, complained, or blocked:** no email goes out. [After a send](06-after-a-send.md) covers those addresses.
|
|
25
|
-
- **Erased at their request:** they get a confirmation, and confirming lifts the erasure, so they're back on your list.
|
|
13
|
+
- **Already subscribed, bounced, complained, or blocked:** no email goes out.
|
|
14
|
+
- **Waiting to confirm:** at most one confirmation every 15 minutes, however often the form is submitted. A link works for 7 days, and an expired one offers a fresh link.
|
|
15
|
+
- **Unsubscribed earlier, or erased at their request:** they get a confirmation, and confirming puts them back on your list.
|
|
26
16
|
|
|
27
17
|
## The confirmation email
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
The section shows the email as a reader gets it. Choose **Edit** to change four things:
|
|
32
|
-
|
|
33
|
-
- **Subject line**
|
|
34
|
-
- **Message**, the line above the confirm button
|
|
35
|
-
- **Button label**
|
|
36
|
-
- **Reassurance line**, a quiet footer for anyone who didn't sign up. Leave it blank to omit it.
|
|
37
|
-
|
|
38
|
-
A required field left blank uses Kestrel's default wording, and **Reset to Kestrel default** brings all of it back. Save with the page's save bar.
|
|
39
|
-
|
|
40
|
-
What you can't change keeps every confirmation working. The email always opens with your publication's name and logo, so a reader sees who's asking first. Kestrel adds the confirm link itself, so no edit can leave it out. The confirmation isn't a post, so it doesn't use your email template and has no unsubscribe link. The words on the page the link opens are fixed too, so what a reader agrees to can't be edited away.
|
|
19
|
+
Change its subject, message, button label, and reassurance line under **Settings → Confirmation email**. Kestrel adds your name, your logo, and the confirm link itself, so no edit can leave them out. The confirmation isn't a post, so it doesn't use your email template.
|
|
41
20
|
|
|
42
21
|
## Put the form on your own site
|
|
43
22
|
|
|
44
|
-
**
|
|
45
|
-
|
|
46
|
-
Choose a look first:
|
|
47
|
-
|
|
48
|
-
- **Styled** looks like your subscribe page, with its own fonts and colors carried along, so it fits anywhere.
|
|
49
|
-
- **Plain** is bare markup that takes your site's styles, or the browser's own controls where your site has none.
|
|
50
|
-
|
|
51
|
-
The section shows the form as it will look beside the HTML that makes it. Choose **Copy HTML**, then paste it into your site. The form heads itself "Subscribe to" your publication's name.
|
|
52
|
-
|
|
53
|
-
A reader who submits it lands on your Kestrel's own check-your-inbox page, and confirms by email as on the subscribe page.
|
|
54
|
-
|
|
55
|
-
## Preview every reader page
|
|
56
|
-
|
|
57
|
-
**Settings → Reader pages shows each page a reader can land on, exactly as they see it, without walking through the steps.**
|
|
58
|
-
|
|
59
|
-
Pick a page from the list: the subscribe form and its check-your-inbox page, the confirm and unsubscribe pages, or your landing page, archive, and a post page. Pages a reader rarely meets, such as an expired link or an archive with no posts yet, are there too. **Desktop** and **Phone** show each at either width. These are samples, so nothing on them subscribes or unsubscribes anyone.
|
|
60
|
-
|
|
61
|
-
## When signups aren't open yet
|
|
62
|
-
|
|
63
|
-
**The form takes no addresses until whoever runs your Kestrel connects email.**
|
|
64
|
-
|
|
65
|
-
Until then, the subscribe page says the newsletter isn't taking subscribers yet, and the dashboard and Settings say so too. You can still share the link, and it opens once email is connected. [Connect Amazon SES](../get-started/04-ses.md) is that step.
|
|
66
|
-
|
|
67
|
-
Whoever runs your Kestrel can also limit how often anyone submits the form, which slows down abuse. [Rate-limit the subscribe form](../guides/05-rate-limit.md) covers it.
|
|
68
|
-
|
|
69
|
-
## Already have a list somewhere else
|
|
70
|
-
|
|
71
|
-
**Import it instead of asking everyone to sign up again.** Readers who opted in with your old service come with that record of their consent. [Bring your existing list](01-your-list.md) covers importing and exporting.
|
|
23
|
+
**Settings → Ways to subscribe → Embed a form** gives you HTML to paste into any page you control. **Styled** looks like your subscribe page, and **Plain** takes your site's own styles. A reader who submits it confirms by email, as on the subscribe page.
|
|
72
24
|
|
|
73
|
-
|
|
25
|
+
**Settings → Reader pages** shows every page a reader can land on, at desktop and phone widths, without walking through the steps. You can also limit how often anyone submits the form, which slows down abuse ([Rate-limit the subscribe form](../guides/05-rate-limit.md)).
|
|
@@ -1,48 +1,30 @@
|
|
|
1
1
|
# Check your setup
|
|
2
2
|
|
|
3
|
-
Before your readers get anything, see it arrive yourself, and make sure a problem will reach you without your watching for it. Send yourself a test, subscribe yourself, and set where Kestrel emails you.
|
|
3
|
+
Before your readers get anything, see it arrive yourself, and make sure a problem will reach you without your watching for it. Send yourself a test and reply to it, subscribe yourself, and set where Kestrel emails you.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Email is connected
|
|
8
|
-
|
|
9
|
-
If nothing on your dashboard says otherwise, it is. While no email provider is connected, Kestrel says so wherever a send would fail. Until your first post, the dashboard's checklist shows **Connect Amazon SES** in place of this step. After that, an amber line on the dashboard reads "No email provider connected, so nothing can be sent yet." The Schedule dialog says the post can't be scheduled yet, and its buttons are unavailable.
|
|
10
|
-
|
|
11
|
-
**Settings → Email sender** shows your From address, sending domain, and email provider, read-only. Connecting a provider is for whoever runs your Kestrel, in [Connect Amazon SES](../get-started/04-ses.md) and [Verify it works](../get-started/05-verify.md).
|
|
5
|
+
If you're bringing a list of more than about 5,000 subscribers, upgrade to Cloudflare's Workers Paid plan before you import it. On the free plan, a large import and a send can use up the day's database writes. Then the app stops working until midnight UTC. [As your list grows](../guides/09-as-your-list-grows.md) walks through the upgrade.
|
|
12
6
|
|
|
13
7
|
## Send yourself a test
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
Choose **Template** in the sidebar, then **Send test email**, and send a sample post to your own inboxes. When it arrives, check:
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
-
- **Your logo and name,** as your template prints them.
|
|
21
|
-
- **Where it lands:** the inbox, Promotions, or spam. If it lands in spam, tell whoever runs your Kestrel. [Sending-domain DNS](../reference/02-sending-domain-dns.md) covers what they check.
|
|
22
|
-
- **A reply.** Reply to the test, and check that it reaches your inbox. Kestrel never sees replies, so this depends on [forwarding set up by whoever runs your Kestrel](../get-started/04-ses.md#3-receive-replies-at-your-from-address).
|
|
11
|
+
- **The From name and address,** which readers see first. They're set when Kestrel is deployed ([Connect Amazon SES](../get-started/04-ses.md#8-set-the-provider-and-deploy)).
|
|
12
|
+
- **Where it lands:** the inbox, Promotions, or spam. If it lands in spam, check your sending domain's records, as [Sending-domain DNS](../reference/02-sending-domain-dns.md) describes.
|
|
13
|
+
- **A reply.** Reply to the test, and check that it reaches your inbox. Kestrel never sees replies, so this depends on [forwarding your From address](../get-started/04-ses.md#3-receive-replies-at-your-from-address).
|
|
23
14
|
|
|
24
15
|
## Subscribe yourself
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
Open your subscribe page, from the link under **Settings → Ways to subscribe**, and subscribe with a personal address. Then confirm. You see what a new reader sees, and you then appear under **Subscribers** as **Confirmed**. [Share your subscribe link](09-share-your-subscribe-link.md) covers the page and the confirmation email.
|
|
27
18
|
|
|
28
|
-
##
|
|
19
|
+
## Set your notifications address
|
|
29
20
|
|
|
30
|
-
|
|
21
|
+
Under **Settings → Notifications**, fill in **Send notifications to**, save, and choose **Send a test notification**. Kestrel then emails you when a send finishes or needs you. [Notifications](06-troubleshooting.md#notifications) lists what you're told, and why a second way to send them is worth asking for.
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
- **The dashboard's health block.** It appears only when something needs attention, and stays until it clears. Red needs you to act, and amber is worth a look.
|
|
34
|
-
- **Warnings when you schedule.** The Schedule dialog warns you before you schedule something that could go wrong. [Scheduling and sending](05-scheduling-and-sending.md#warnings-in-the-schedule-dialog) covers them.
|
|
23
|
+
## If email isn't connected
|
|
35
24
|
|
|
36
|
-
|
|
25
|
+
Until an email provider is connected, nothing can be sent, and the dashboard says so. The checklist shows **Connect Amazon SES** in place of this step, as [Connect Amazon SES](../get-started/04-ses.md) describes. **Settings → Email sender** shows your From address, sending domain, and provider, read-only.
|
|
37
26
|
|
|
38
27
|
## As your list grows
|
|
39
28
|
|
|
40
|
-
A growing list meets
|
|
41
|
-
|
|
42
|
-
- **Your provider's daily sending quota.** A send that hits it waits, and the dashboard shows why. It resumes on its own once the quota frees up, and nobody is mailed twice. Amazon SES reports a spent quota as refusing your account, so its email reaches you only if your notifications go another way ([Notifications](06-after-a-send.md#notifications)).
|
|
43
|
-
- **How long a send takes.** It grows with your list, and depends on your provider's sending rate and the hosting plan whoever runs your Kestrel chose. [Go live](../get-started/06-go-live.md#as-your-list-grows) gives the rates.
|
|
44
|
-
- **Daily database writes on the free hosting plan.** A large send or import can use up the day's writes. Then the whole app stops until midnight UTC. On that plan, Kestrel warns you before a send to more than 5,000 recipients. It also warns you before an import that would use most of the day's writes. Neither warning stops you. [Bring your existing list](01-your-list.md#limits) explains more.
|
|
45
|
-
|
|
46
|
-
From about 5,000 subscribers, it's recommended that whoever runs your Kestrel moves to the paid hosting plan, which has no daily limit on writes.
|
|
29
|
+
A growing list meets your Amazon SES daily sending quota, longer sends, and on Cloudflare's free Workers plan, a cap on daily database writes. Kestrel tells you when you reach each one, and [As your list grows](../guides/09-as-your-list-grows.md) covers raising each.
|
|
47
30
|
|
|
48
|
-
Next, [Bring your existing list](01-your-list.md).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Edit your email template
|
|
2
|
+
|
|
3
|
+
You write each post in Markdown, and the template is the HTML frame it's sent in, like a layout in a static site generator. `{{ .Post.Body }}` marks where your post goes, and the rest is plain HTML around it. The built-in template works as it comes, so changing it is optional.
|
|
4
|
+
|
|
5
|
+
To change it, choose **Template** in the sidebar, edit the HTML, and choose **Save changes**. **Start from example** offers two starting points. **Signed** ends each post with your logo, name, and tagline, and **Plain** has just your post and the footer links.
|
|
6
|
+
|
|
7
|
+
## What every template needs
|
|
8
|
+
|
|
9
|
+
Kestrel won't save a template without `{{ .Post.Body }}` and `{{ .Email.UnsubscribeURL }}`. Every email has to carry the post and a way to leave. The two **Required** pills above the editor turn green or red as your template gains or loses them.
|
|
10
|
+
|
|
11
|
+
Wrap a part in `{{ if .IsEmail }}` and `{{ end }}` to keep it off each post's public page. The starting templates do this with the unsubscribe link, the View in browser link, and the mailing address.
|
|
12
|
+
|
|
13
|
+
## Variables
|
|
14
|
+
|
|
15
|
+
A variable is replaced with its value in each email. Type it exactly as shown, or it renders as empty.
|
|
16
|
+
|
|
17
|
+
| Variable | What it holds |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `{{ .Post.Body }}` | Your post, rendered from Markdown. Required. |
|
|
20
|
+
| `{{ .Post.Subject }}` | The post's subject line. |
|
|
21
|
+
| `{{ .Publication.Name }}` | Your publication's name. |
|
|
22
|
+
| `{{ .Publication.Tagline }}` | Your tagline. |
|
|
23
|
+
| `{{ .Publication.Logo }}` | Your logo as an image, or nothing when no logo is set. |
|
|
24
|
+
| `{{ .Publication.LogoURL }}` | Your logo's address, or empty. |
|
|
25
|
+
| `{{ .Publication.Address }}` | Your mailing address, or empty. |
|
|
26
|
+
| `{{ .Email.SentTo }}` | The address this email was sent to. |
|
|
27
|
+
| `{{ .Email.UnsubscribeURL }}` | This reader's one-click unsubscribe link. Required. |
|
|
28
|
+
| `{{ .Email.ViewInBrowserURL }}` | The post's public page. |
|
|
29
|
+
|
|
30
|
+
## Test it
|
|
31
|
+
|
|
32
|
+
**Send test email** on the **Template** page sends a sample post through your saved template, so you see it in a real inbox. With unsaved edits, the button reads **Save & send test**, since a test always uses the saved template.
|
|
33
|
+
|
|
34
|
+
A change to the template re-makes the email of every scheduled post, as [Schedule and send](04-write-a-post.md#schedule-and-send) describes.
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
# Publishing with Kestrel
|
|
2
2
|
|
|
3
|
-
These pages
|
|
3
|
+
These pages get you publishing in the dashboard, and cover the decisions to make before they matter. Deploying and running Kestrel itself is covered in [Self-hosting](../README.md).
|
|
4
4
|
|
|
5
5
|
## Set up your publication
|
|
6
6
|
|
|
7
|
-
The steps
|
|
7
|
+
The steps to set up a new publication, in the order the dashboard's setup checklist lists them.
|
|
8
8
|
|
|
9
9
|
1. [Name your publication](03-name-your-publication.md)
|
|
10
10
|
1. [Check your setup](10-check-your-setup.md)
|
|
11
11
|
1. [Bring your existing list](01-your-list.md)
|
|
12
|
-
1. [Write
|
|
12
|
+
1. [Write and send a post](04-write-a-post.md)
|
|
13
13
|
1. [Share your subscribe link](09-share-your-subscribe-link.md)
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Go further
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Things you choose to do once you're running.
|
|
18
18
|
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [Sending rules](02-sending-rules.md)
|
|
19
|
+
- [Edit your email template](11-email-template.md)
|
|
20
|
+
- [Working with Claude](07-working-with-claude.md)
|
|
22
21
|
|
|
23
|
-
##
|
|
22
|
+
## Rules and troubleshooting
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
What the law asks of every send, and what to do when something goes wrong.
|
|
25
|
+
|
|
26
|
+
- [Sending rules](02-sending-rules.md)
|
|
27
|
+
- [Troubleshooting](06-troubleshooting.md)
|
|
@@ -30,7 +30,7 @@ None of these can be read or changed from inside the app, by the editor or by Cl
|
|
|
30
30
|
|
|
31
31
|
### `PROVIDER`
|
|
32
32
|
|
|
33
|
-
Until it's set, or while it's empty, the app runs with no provider connected.
|
|
33
|
+
Until it's set, or while it's empty, the app runs with no provider connected. Posts can be written and previewed, but everything that would send email is refused: scheduling, sending now, test emails, and the confirmation emails that signups need. The subscribe page says signups aren't open yet, and Settings shows **No email provider connected**. Setting it to `none` means the same. A new instance's `production` block declares it empty, `"PROVIDER": ""`, so wrangler doesn't warn that the top level's value is missing there. [Connect Amazon SES](../get-started/04-ses.md) changes it to `ses`.
|
|
34
34
|
|
|
35
35
|
`fake` delivers nothing. It records a send as if every recipient accepted it. Local development runs on it, and names it on purpose in the top level of `wrangler.jsonc`. Never copy it into `production`, whatever wrangler suggests: every subscriber would be recorded as sent the post, and never get it. The app refuses `fake` unless `APP_ORIGIN` is on `localhost` or `127.0.0.1`. A hosted demo that runs `fake` on a real hostname on purpose sets `ALLOW_FAKE_PROVIDER` to `true`, which no other deployment needs, and which no other provider reads.
|
|
36
36
|
|
|
@@ -38,11 +38,11 @@ Until it's set, or while it's empty, the app runs with no provider connected. Yo
|
|
|
38
38
|
|
|
39
39
|
`FROM_ADDRESS` is the only address mail is sent from, and the identity your provider authenticates. `SENDING_DOMAIN` is for display: Settings shows it beside the From address. The app never checks that the two match, so set `SENDING_DOMAIN` to the part of `FROM_ADDRESS` after the `@`.
|
|
40
40
|
|
|
41
|
-
Neither one is
|
|
41
|
+
Neither one is the publication's name, tagline, or logo. You set those in the editor, under **Settings → Publication identity**. Until you do, the From address's display name stands in for the name, and with neither, the emails and reader pages show no name at all.
|
|
42
42
|
|
|
43
43
|
### `ARCHIVE_BASE_PATH`
|
|
44
44
|
|
|
45
|
-
Every post
|
|
45
|
+
Every post sent carries its archive link, `ARCHIVE_BASE_PATH` followed by the post's slug, and those links are permanent. Changing the prefix later breaks every link already mailed, so pick it before the first send. To keep links from an older install working, set it to the prefix they use, such as `/newsletter`.
|
|
46
46
|
|
|
47
47
|
A missing leading slash is added, and a trailing slash is dropped.
|
|
48
48
|
|
|
@@ -64,9 +64,9 @@ This setting is the subrequest cap. The app keeps database queries under theirs
|
|
|
64
64
|
|
|
65
65
|
It's also how the app tells Workers Free from Workers Paid. At the default, the app runs on Free's limits:
|
|
66
66
|
|
|
67
|
-
- One import takes up to 1,000 rows, since Free's 10 ms of CPU a request can't plan and commit more. Above it, up to 10,000 ([
|
|
67
|
+
- One import takes up to 1,000 rows, since Free's 10 ms of CPU a request can't plan and commit more. Above it, up to 10,000 ([Limits in detail](../publishing/01-your-list.md#limits-in-detail)).
|
|
68
68
|
- An export reads 1,000 rows a page. Above it, 10,000.
|
|
69
|
-
- The app warns before a send to more than 5,000 recipients (fewer on a day
|
|
69
|
+
- The app warns before a send to more than 5,000 recipients (fewer on a day with imports), and on an import plan that, with the day's earlier imports, would use most of a day's writes. Either may use up D1's daily allowance of 100,000 rows written, which stops the app until midnight UTC ([As your list grows](../guides/09-as-your-list-grows.md)). Neither warning stops you. Above the default, neither appears.
|
|
70
70
|
|
|
71
71
|
Never set it above your plan's limit. A minute that hits the cap is cut off mid-batch, and the send stalls until a later minute picks it up. A value below 30 is raised to 30, the least a minute needs to send anything and still send a notification.
|
|
72
72
|
|
|
@@ -74,7 +74,7 @@ Never set it above your plan's limit. A minute that hits the cap is cut off mid-
|
|
|
74
74
|
|
|
75
75
|
Every send stays visible and cancelable for at least this long before it goes out, whether it's scheduled, sent now, or moved. It's the window to catch a mistake, including one in a send Claude prepared.
|
|
76
76
|
|
|
77
|
-
The floor is 60, since sends go out from a once-a-minute schedule. The ceiling is 86400, one day. It's the least wait before every send, not a limit on how far ahead
|
|
77
|
+
The floor is 60, since sends go out from a once-a-minute schedule. The ceiling is 86400, one day. It's the least wait before every send, not a limit on how far ahead a post can be scheduled. It lives in deploy config on purpose, so neither the editor nor Claude can shorten it.
|
|
78
78
|
|
|
79
79
|
### `ARCHIVE_ORIGIN` and `MEDIA_PUBLIC_BASE`
|
|
80
80
|
|
|
@@ -27,7 +27,7 @@ SES generates the DKIM values. Copy them exactly as SES lists them, since they d
|
|
|
27
27
|
|
|
28
28
|
SPF needs no record of yours on SES's default path, since it passes for a subdomain of `amazonses.com`. An SPF record, at `bounce.send`, comes only with the optional custom MAIL FROM domain.
|
|
29
29
|
|
|
30
|
-
Email Routing adds MX, SPF, and DKIM records at `send` itself, the DKIM one under the selector `cf2024-1`. They're for mail sent *to* your From address, so readers' replies reach
|
|
30
|
+
Email Routing adds MX, SPF, and DKIM records at `send` itself, the DKIM one under the selector `cf2024-1`. They're for mail sent *to* your From address, so readers' replies reach the publisher ([Connect Amazon SES](../get-started/04-ses.md#3-receive-replies-at-your-from-address)). They don't affect your sending: inboxes check SES's mail for SPF at its return path, and for DKIM against SES's own keys.
|
|
31
31
|
|
|
32
32
|
### SPF: authorize the return path
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kurtbruns/kestrel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An open-source, self-hosted newsletter app where you own the list and the archive. Work in the editor, or just ask Claude. Runs on Cloudflare Workers.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,12 +8,12 @@ argument-hint: "[path to the export]"
|
|
|
8
8
|
|
|
9
9
|
An import adds people to the list, and a wrong guess about who agreed to be emailed can't be taken back. So this skill holds no facts about any service's export, which change without notice. It holds a method that checks itself against the file in hand and against Kestrel's own plan.
|
|
10
10
|
|
|
11
|
-
The
|
|
11
|
+
The authority on the import format is the "The file format" section of the publisher's page "Bring your existing list": `GET /api/docs/your-list.md` on the instance, or `node_modules/@kurtbruns/kestrel/docs/publishing/01-your-list.md`. If anything here seems to disagree with it or with the API, they win; say so.
|
|
12
12
|
|
|
13
13
|
## Before you start
|
|
14
14
|
|
|
15
15
|
- **Read the truth from the instance, every time.** Call `GET /api/imports/format` and `GET /api/reference` at the start of each migration. They give the columns, the values each takes, the outcomes, the attestation, and this deployment's `limits`. Never write the format from memory, or from this file.
|
|
16
|
-
- **Explain it in Kestrel's words.** For what to tell the publisher about the format, the row limit, and the Workers Free advice, read the
|
|
16
|
+
- **Explain it in Kestrel's words.** For what to tell the publisher about the format, the row limit, and the Workers Free advice, read the rest of that page, above "The file format", with this deployment's numbers from `GET /api/imports/format`. Use them rather than your own account of the format.
|
|
17
17
|
- **Convert to the format the route names as `preferred`,** the full CSV. Fall back to a plain list only when the export holds nothing but addresses, and tell the publisher what a plain list loses, from the route's own `loses`.
|
|
18
18
|
- **Work outside the repository.** Make a working folder outside this repository, such as `~/kestrel-migration/`, for the export, the converter, its tests, and the converted pieces. They hold real addresses, so nothing from the list is ever committed here. A fixture that must be committed uses made-up rows only.
|
|
19
19
|
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
# Try Kestrel locally first
|
|
2
|
-
|
|
3
|
-
You can run Kestrel on your own computer to look around before you deploy it. It needs no Cloudflare account and no Git host. Email goes to a stand-in on your computer, so nothing reaches a real inbox.
|
|
4
|
-
|
|
5
|
-
In this guide, you write a new instance into an empty folder, start the app, and open the editor. Then you fill it with a demo publication, and empty it again. The folder can become your instance's repository later.
|
|
6
|
-
|
|
7
|
-
## Before you begin
|
|
8
|
-
|
|
9
|
-
- [Node.js](https://nodejs.org/) 22 or later
|
|
10
|
-
|
|
11
|
-
## 1. Write a new instance
|
|
12
|
-
|
|
13
|
-
An instance is a small folder of your own. It holds the instance's settings and names the release of Kestrel it runs, which installs from npm as `@kurtbruns/kestrel`.
|
|
14
|
-
|
|
15
|
-
1. Make an empty folder, and go into it:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
mkdir newsletter
|
|
19
|
-
cd newsletter
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
1. Write a new instance into it:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npx @kurtbruns/kestrel init
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
It writes the instance's files, and lists the commands to run next. The folder doesn't need to be a Git repository.
|
|
29
|
-
|
|
30
|
-
1. Install the release and its tools:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm install
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## 2. Start the app
|
|
37
|
-
|
|
38
|
-
1. Copy the local settings:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
cp .dev.vars.example .dev.vars
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
They're for your computer only, and send email to the stand-in.
|
|
45
|
-
|
|
46
|
-
1. Start the app:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm run dev
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The first time, it creates the local database and asks you to confirm its migrations. Answer yes. The server keeps running in this terminal, and its output ends with the address it's ready on.
|
|
53
|
-
|
|
54
|
-
1. Open http://localhost:8787/dashboard/ to see the editor. There's nothing to sign in with on your computer. If another app already uses port 8787, Kestrel picks a free port instead, and the `Ready on` line shows it.
|
|
55
|
-
|
|
56
|
-
## 3. Load the demo publication
|
|
57
|
-
|
|
58
|
-
The demo fills the editor with posts, subscribers, and sends that already went out, so you can see the app in use.
|
|
59
|
-
|
|
60
|
-
1. In a second terminal, in the same folder, load the demo:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
npm run seed
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
It replaces everything in the local database, and lists links to the editor and the demo's published posts.
|
|
67
|
-
|
|
68
|
-
1. To empty the local database again, and see the app as a new instance starts:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npm run reset
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
1. To stop the server, press <kbd>Ctrl</kbd>+<kbd>C</kbd> in its terminal. Your local content stays for the next `npm run dev`.
|
|
75
|
-
|
|
76
|
-
## Check it
|
|
77
|
-
|
|
78
|
-
1. The app answers. If `npm run dev` picked another port, use the one on its `Ready on` line here and below:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
curl http://localhost:8787/health
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
{"status":"ok","service":"kestrel"}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
1. After `npm run seed`, http://localhost:8787/ shows the demo publication's public landing page, with its posts.
|
|
89
|
-
|
|
90
|
-
## Keep the folder for your instance
|
|
91
|
-
|
|
92
|
-
If you came here from Deploy the app, your folder is already a repository: go back to [Sign in to Cloudflare](../get-started/02-deploy.md#2-sign-in-to-cloudflare).
|
|
93
|
-
|
|
94
|
-
When you're ready to deploy, this folder can be your instance's repository. You need [Git](https://git-scm.com/) and a GitHub account for this part. The folder's `.gitignore` already keeps your local settings, the local database, and the installed packages out of it.
|
|
95
|
-
|
|
96
|
-
1. On GitHub, [create a new repository](https://github.com/new) named `newsletter`. It can be private. Leave it empty, with no README, license, or `.gitignore`.
|
|
97
|
-
|
|
98
|
-
1. Make the folder a repository, and commit the instance. Replace `YOUR_USERNAME` with your GitHub username:
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
git init
|
|
102
|
-
git add -A
|
|
103
|
-
git commit -m "Start a Kestrel instance"
|
|
104
|
-
git remote add origin https://github.com/YOUR_USERNAME/newsletter.git
|
|
105
|
-
git push -u origin HEAD
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Then check [what deploying needs](../get-started/02-deploy.md#before-you-begin) and [choose your hostnames](../get-started/02-deploy.md#choose-your-hostnames). Then [deploy the app](../get-started/02-deploy.md#2-sign-in-to-cloudflare) from signing in to Cloudflare onward.
|