@kurtbruns/kestrel 1.8.0 → 1.10.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 +66 -1
- package/README.md +4 -2
- package/cli/context.mjs +5 -1
- package/dist/public/dashboard/app.js +366 -255
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +512 -20
- package/dist/worker/index.js +3046 -632
- package/docs/README.md +3 -3
- package/docs/get-started/04-ses.md +54 -14
- package/docs/get-started/05-verify.md +5 -3
- package/docs/get-started/06-go-live.md +10 -5
- package/docs/guides/01-connect-claude.md +4 -0
- package/docs/guides/03-notifications.md +1 -1
- package/docs/publishing/01-your-list.md +86 -0
- package/docs/publishing/02-sending-rules.md +35 -0
- package/docs/publishing/03-name-your-publication.md +106 -0
- package/docs/publishing/04-write-a-post.md +98 -0
- package/docs/publishing/05-scheduling-and-sending.md +72 -0
- package/docs/publishing/06-after-a-send.md +109 -0
- package/docs/publishing/07-working-with-claude.md +48 -0
- package/docs/publishing/08-import-format.md +103 -0
- package/docs/publishing/09-share-your-subscribe-link.md +73 -0
- package/docs/publishing/10-check-your-setup.md +48 -0
- package/docs/publishing/README.md +26 -0
- package/docs/reference/01-configuration.md +7 -1
- package/docs/reference/02-sending-domain-dns.md +4 -2
- package/migrations/0005_imports.sql +74 -0
- package/migrations/0006_deliveries_fewer_indexes.sql +19 -0
- package/package.json +2 -1
- package/template/claude/CLAUDE.md +5 -2
- package/template/claude/skills/migrate-list/SKILL.md +56 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Write your first post
|
|
2
|
+
|
|
3
|
+
You write a post in Markdown, in the editor. Kestrel saves it as you type and shows you the exact email under **Preview**. A test sends that same email to any inbox you name.
|
|
4
|
+
|
|
5
|
+
This page covers the subject and slug, the Markdown Kestrel reads, images, and saving. It also covers sharing a post with Claude or another tab, the preview, and test emails. It stops where scheduling starts.
|
|
6
|
+
|
|
7
|
+
## Start a post
|
|
8
|
+
|
|
9
|
+
**New post**, on the Dashboard or on **Drafts**, creates a draft and opens it in the editor. A draft stays under **Drafts** until you schedule it, and you can edit it as often as you like until then.
|
|
10
|
+
|
|
11
|
+
To remove a draft, choose **Delete draft** from its ⋯ menu under **Drafts**. Its saved versions and images go with it, and it can't be undone.
|
|
12
|
+
|
|
13
|
+
## The subject and the slug
|
|
14
|
+
|
|
15
|
+
The **Subject** is the email's subject line. It also names the post in your lists and sets its web address. A post needs a subject before you can schedule it.
|
|
16
|
+
|
|
17
|
+
The **Slug** is the last part of the post's public address. Every post you send gets a permanent page in your publication's archive, and every email links to it. With the default setup, a post with the slug `spring-update` lives at `newsletter.example.com/archive/spring-update`. Whoever runs your Kestrel chose the part before the slug, as [Configuration](../reference/01-configuration.md#archive_base_path) describes.
|
|
18
|
+
|
|
19
|
+
While **Auto-generate from subject** is checked, the slug follows the subject, so "Spring update" becomes `spring-update`. Type a slug of your own to set it by hand, and it stops following the subject. Clear the field, or check the box again, to have it follow the subject.
|
|
20
|
+
|
|
21
|
+
A slug uses lowercase letters, numbers, and hyphens, and Kestrel converts what you type to that form. No two posts share a slug, drafts included. When the one a post would take is already held, Kestrel adds a number, such as `spring-update-2`. The field shows the slug the post really has, so it can update a moment after you change the subject.
|
|
22
|
+
|
|
23
|
+
A draft's slug can change until you schedule it, and a test email sent before a change links to the old address. Once the post is scheduled, its address never moves. **Web version ↗** shows the post's public page as it will look, and the address it will have.
|
|
24
|
+
|
|
25
|
+
## Markdown
|
|
26
|
+
|
|
27
|
+
Kestrel reads Markdown in GitHub's flavor and turns it into the body of your email template. The toolbar covers the common cases: **Heading**, **Bold**, **Italic**, **Quote**, **Code**, **Link**, **Bulleted list**, **Numbered list**, and **Indent**. ⌘B, ⌘I, and ⌘K work too.
|
|
28
|
+
|
|
29
|
+
Tables and strikethrough work as well, since they're part of GitHub's Markdown. How a mail client draws them varies, and so does code, so check anything unusual in a test email.
|
|
30
|
+
|
|
31
|
+
A few things work differently in an email:
|
|
32
|
+
|
|
33
|
+
- **Line breaks.** A single line break inside a paragraph doesn't start a new line. Leave a blank line between paragraphs.
|
|
34
|
+
- **HTML.** You can write HTML where Markdown falls short. Kestrel removes scripts, `<style>` blocks, forms, and event handlers, since no email needs them. Style a piece with an inline `style` attribute instead.
|
|
35
|
+
- **The inbox preview.** The snippet a mail client shows beside the subject comes from the start of your post. There's no separate field for it, so make the opening lines count.
|
|
36
|
+
- **The plain-text version.** Every email also carries a plain-text version made from the same post. A link becomes its text followed by its address, and an image becomes its alt text.
|
|
37
|
+
|
|
38
|
+
## Images
|
|
39
|
+
|
|
40
|
+
To add an image, paste it, drop it on the composer, or click **Paste, drop, or click to add images** below it. Kestrel uploads the file to the post and inserts a reference by its file name, such as ``. The reference is only the name, never an address to paste.
|
|
41
|
+
|
|
42
|
+
The words in square brackets are the image's alt text, which screen readers read aloud and many mail clients show while images are blocked. Kestrel starts it as the file name, so replace that with a short description: ``.
|
|
43
|
+
|
|
44
|
+
- **Formats.** PNG, JPEG, WebP, or GIF. SVG isn't accepted, since most email apps don't show it.
|
|
45
|
+
- **Size.** Up to 5 MB per image. Kestrel doesn't shrink the file, and every reader downloads it, so a smaller file opens faster. An image wider than the email's column is shown at the column's width.
|
|
46
|
+
- **Warnings.** The preview and each test warn you about an image with no alt text, or a reference to a file the post doesn't have.
|
|
47
|
+
|
|
48
|
+
Images are public, so any mail client and your archive page can load them. You can add or replace images only while the post is a draft. Uploading a file with the same name as an earlier one replaces it. Once a post is scheduled its images are fixed, and a sent post's images stay, so its emails and archive page keep showing them. Deleting a draft deletes its images.
|
|
49
|
+
|
|
50
|
+
## Saving and versions
|
|
51
|
+
|
|
52
|
+
The editor saves on its own, after a short pause in your typing and never more than half a minute late. **Save draft**, or ⌘S, saves at once.
|
|
53
|
+
|
|
54
|
+
Beside **Save draft**, the save cue says where you stand: Unsaved, Saving…, or Saved. If a save fails, the cue turns red with "Not saved:" and the reason, until a save succeeds. After a dropped connection the editor tries again on its own.
|
|
55
|
+
|
|
56
|
+
Every save keeps a whole version of the post. The editor doesn't show that history, but Claude can read it, as [Working with Claude](07-working-with-claude.md) describes.
|
|
57
|
+
|
|
58
|
+
## When someone else edits the same post
|
|
59
|
+
|
|
60
|
+
Kestrel never lets one save silently overwrite another. That matters when the same post is open in a second tab or browser, or Claude is editing it for you.
|
|
61
|
+
|
|
62
|
+
When another writer saves a newer version, a banner says so and names who, with Claude's saves shown as "Claude". It appears while you're still writing, not only when you save. Saving pauses until you choose:
|
|
63
|
+
|
|
64
|
+
- **Reload** loads their version, and discards your unsaved edits.
|
|
65
|
+
- **Keep editing** keeps yours, and your next save replaces their version.
|
|
66
|
+
|
|
67
|
+
Both choices lose something, so read what changed before you pick. If the post is scheduled elsewhere, the banner says it can no longer be edited here. If it's deleted elsewhere, the editor says so and returns to **Drafts**.
|
|
68
|
+
|
|
69
|
+
## Preview
|
|
70
|
+
|
|
71
|
+
**Preview** shows the post as the email, made by the same code that makes your test emails and the send itself. It saves your latest edits first, so what you see is the post as it stands. Any warnings, such as a missing subject or alt text, show beneath it.
|
|
72
|
+
|
|
73
|
+
Three views show the post, each for one purpose:
|
|
74
|
+
|
|
75
|
+
- **Preview** is the email, for a quick look at layout.
|
|
76
|
+
- **Send test email** is the email in a real inbox, where rendering finally counts.
|
|
77
|
+
- **Web version ↗** is the post's public archive page, before it's public.
|
|
78
|
+
|
|
79
|
+
Inside the preview, the email's "view in browser" link opens the web version, since the public page doesn't exist until the post sends. Mail clients each draw email a little differently, so trust a test over the preview.
|
|
80
|
+
|
|
81
|
+
## Test emails
|
|
82
|
+
|
|
83
|
+
**Send test email** opens a dialog that takes one or more addresses, one per line. Each gets its own copy of the email, and nobody on your list receives anything. The addresses in **Settings → Default test recipients** fill the dialog for you.
|
|
84
|
+
|
|
85
|
+
A test is the real email. It saves your draft first, then sends it through the same template and code as the send to your list. So a clean test means a clean send. Once a post is scheduled, a test sends the exact copy that will go out.
|
|
86
|
+
|
|
87
|
+
When some addresses don't get the test, the dialog stays open with only those left, and says why. **Send test** again tries just those, so no one gets a second copy.
|
|
88
|
+
|
|
89
|
+
A few links in a test behave differently from the real send:
|
|
90
|
+
|
|
91
|
+
- **Unsubscribe.** A test carries an unsubscribe link and the one-click unsubscribe header, so you can check both. Neither unsubscribes anyone. The link opens the page a subscriber sees, marked as a test and showing a sample address.
|
|
92
|
+
- **View in browser.** The link points at the post's public address, which shows a page saying the post appears there once it's sent. It works once the send starts.
|
|
93
|
+
|
|
94
|
+
A test needs an email provider. Until whoever runs your Kestrel connects one, the editor lets you write and preview but not send tests. [Connect Amazon SES](../get-started/04-ses.md) is their step.
|
|
95
|
+
|
|
96
|
+
## Next
|
|
97
|
+
|
|
98
|
+
When the test looks right, [Scheduling and sending](05-scheduling-and-sending.md) covers putting the post on the schedule. On a new publication, finish setting up first: [Share your subscribe link](09-share-your-subscribe-link.md).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Scheduling and sending
|
|
2
|
+
|
|
3
|
+
Every post goes out on a schedule. Scheduling makes the email and locks the post, and the send stays visible and cancelable until its time comes. That window is your review, and nothing can skip it, not even **Send now**.
|
|
4
|
+
|
|
5
|
+
This page covers scheduling, what it freezes, canceling, moving the send time, and sending now. It also covers who receives a post, and what happens when it goes out.
|
|
6
|
+
|
|
7
|
+
## Scheduling a post
|
|
8
|
+
|
|
9
|
+
**Schedule** at the foot of the editor opens a dialog where you pick a time under **Send at**, then confirm with **Schedule**. The post goes out at that time, and until then you can cancel it or move it.
|
|
10
|
+
|
|
11
|
+
The time you pick must be at least the minimum lead away. That's five minutes, unless whoever runs your Kestrel set it differently ([Configuration](../reference/01-configuration.md#min_lead_seconds)). A time closer than that is refused under the picker, with the reason. The lead isn't a setting in the dashboard, and Claude can't change it either. So every send gets a window to catch a mistake, including one Claude prepared.
|
|
12
|
+
|
|
13
|
+
Send times are to the minute. A time with seconds is rounded up to the next whole minute, which only lengthens the window.
|
|
14
|
+
|
|
15
|
+
A post needs a subject to schedule. If the subject is empty, pressing **Schedule** marks the **Subject** field with "Add a subject before you schedule." An empty body gets a warning, not a refusal.
|
|
16
|
+
|
|
17
|
+
After you schedule, you stay on the post. A toast names the send time and suggests you send yourself a test, since the window is where you review. The post now shows the scheduled banner: "Scheduled for" the time, with a countdown ("Sends in …"), and **Reschedule** and **Cancel** beside it.
|
|
18
|
+
|
|
19
|
+
You manage a scheduled post from that banner, or from the **Scheduled** card on **Sent**. The dashboard's **Scheduled** card shows the countdown and opens the post.
|
|
20
|
+
|
|
21
|
+
## What scheduling freezes
|
|
22
|
+
|
|
23
|
+
Scheduling makes the email once, from the post, your email template, and your publication's details as they are at that moment. That frozen copy is what goes out, and it's also the post's page in your archive.
|
|
24
|
+
|
|
25
|
+
So the post is locked while it's scheduled. It opens on **Preview**, which shows the frozen copy. On **Edit**, you can still read, select, and copy the text, and the composer's foot says "Cancel the schedule to edit". **Send test email** still works, and sends you the frozen copy that will go out.
|
|
26
|
+
|
|
27
|
+
To change the post's content, cancel the schedule, edit, test again, and schedule it again. That resets your review, so your last test is always of the copy that sends.
|
|
28
|
+
|
|
29
|
+
A change to your template, or to a publication detail the template shows, is different. It re-makes the email of every scheduled post, at the same send times, with the same content. Kestrel asks before it saves, in a dialog confirmed with **Save and apply**. Afterwards each re-made post's banner asks you to send a new test. The dashboard's card reads "Look changed since your last test" until you do. A save like this is refused while any scheduled post is within the minimum lead of its send time. The refusal says when to try again. [Name your publication](03-name-your-publication.md) covers the template and those details.
|
|
30
|
+
|
|
31
|
+
## Canceling
|
|
32
|
+
|
|
33
|
+
**Cancel** on the scheduled banner, or on the **Scheduled** card on **Sent**, stops the send at once. No confirmation is asked, since nothing is lost: the post is a draft again, editable, and you can schedule it again whenever you like.
|
|
34
|
+
|
|
35
|
+
You can cancel until the send time itself. From then the banner reads "Preparing to send…" and its buttons are unavailable, because the email is going out. A cancel that arrives just after the send time is refused with "Too late to cancel: the send time has passed, so this email is going out."
|
|
36
|
+
|
|
37
|
+
## Moving the send time
|
|
38
|
+
|
|
39
|
+
**Reschedule** moves when a scheduled post sends, without canceling it. It opens the same time picker, and **Reschedule** confirms it. The content stays frozen, your tests still count, and the post stays cancelable until the new time.
|
|
40
|
+
|
|
41
|
+
The new time follows the same rule as scheduling: at least the minimum lead away. Like canceling, you can move a send only before its send time.
|
|
42
|
+
|
|
43
|
+
## Sending now
|
|
44
|
+
|
|
45
|
+
Under the time picker in the Schedule dialog, "Or send as soon as possible." leads to **Send now →**. It's for a post you've already reviewed and want out without picking a time.
|
|
46
|
+
|
|
47
|
+
Sending now still goes through the review window. The **Send now?** view says how many recipients it reaches and when, such as "in 5 to 6 minutes". The range is the minimum lead, plus rounding up to the next minute. Confirm with **Send now**, and the post is locked and scheduled for that time, cancelable until then like any other.
|
|
48
|
+
|
|
49
|
+
## Who receives it
|
|
50
|
+
|
|
51
|
+
A post goes to your list as it stands when it sends: every confirmed subscriber, except addresses that bounced, complained, or were blocked. Someone who confirms while the post is scheduled receives it. Someone who unsubscribes before their email is handed to your email provider doesn't, even partway through the send.
|
|
52
|
+
|
|
53
|
+
So the recipient count the **Send now?** view shows is a snapshot, not a promise. An import that adds subscribers while a post is scheduled adds them to its audience too. The import asks you to acknowledge that first ([Bring your existing list](01-your-list.md#what-an-import-does)).
|
|
54
|
+
|
|
55
|
+
## Warnings in the Schedule dialog
|
|
56
|
+
|
|
57
|
+
The Schedule dialog warns you about two things before you schedule.
|
|
58
|
+
|
|
59
|
+
- **No email provider connected.** The dialog says the post can't be scheduled yet, and its buttons are unavailable. Connecting a provider is for whoever runs your Kestrel, as [Connect Amazon SES](../get-started/04-ses.md) describes. If the provider goes after a post is scheduled, its banner reads "Will be held: no email provider is connected." The post stays cancelable, and goes out once a provider is connected.
|
|
60
|
+
- **Daily database writes.** On the Workers Free plan's limits, a large send may use up the day's database writes, which stops your newsletter until midnight UTC. The dialog warns you, and the post's banner repeats the warning for as long as it's scheduled. It never stops you from scheduling. [Bring your existing list](01-your-list.md#limits) explains the limit and when it's worth moving to Workers Paid.
|
|
61
|
+
|
|
62
|
+
## When it sends
|
|
63
|
+
|
|
64
|
+
At the send time, the post moves from the editor to its send page under **Sent**, where you can watch it go out. If you have the post open, the page switches over with a toast saying why. From here the post can't be edited, canceled, or moved.
|
|
65
|
+
|
|
66
|
+
A send goes out a slice at a time. A large list takes several slices, and between them the send still reads **Sending**, with "Paused between ticks. It resumes on the next sweep."
|
|
67
|
+
|
|
68
|
+
If your email provider refuses to take more, for example because your account's daily sending quota is spent, the send waits. Nobody is marked unsent, and it tries again on its own, picking up where it stopped once the quota frees up. Raising the quota is for whoever runs your Kestrel ([As your list grows](../get-started/06-go-live.md#as-your-list-grows)).
|
|
69
|
+
|
|
70
|
+
Whatever interrupts a send, it resumes rather than starting over. No reader is ever mailed the same post twice.
|
|
71
|
+
|
|
72
|
+
[After a send](06-after-a-send.md) covers what the send page shows once it's out: the record, bounces, complaints, unsent recipients, and your archive page.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# After a send
|
|
2
|
+
|
|
3
|
+
Once a send starts, Kestrel keeps a permanent record of it: who it went to, and what happened to each email. The post goes up on its public page in your archive at the same moment, exactly as it was sent. Nothing about either can be edited, and you don't have to watch: Kestrel emails you when the send finishes, and when something needs you.
|
|
4
|
+
|
|
5
|
+
This page covers a send's record, unsent recipients, bounces and complaints, your notifications, the dashboard's views, and the post's public page.
|
|
6
|
+
|
|
7
|
+
## The send's record
|
|
8
|
+
|
|
9
|
+
Every send has its own page, which you open from **Sent** or from the dashboard's **Sent** list. While the send goes out, the page is a live view of its progress. Once it finishes, the same page becomes its record, in place.
|
|
10
|
+
|
|
11
|
+
The record opens with five counts, over everyone the send went to:
|
|
12
|
+
|
|
13
|
+
| Count | What it means |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| **Recipients** | Everyone the send went to: your confirmed subscribers who weren't suppressed when it started. |
|
|
16
|
+
| **Delivered** | Emails your provider confirmed reached the reader's mail server. |
|
|
17
|
+
| **Bounced** | Emails the receiving server turned away. |
|
|
18
|
+
| **Complained** | Readers who marked the email as spam. |
|
|
19
|
+
| **Unsent** | Recipients Kestrel couldn't hand to your email provider. [Unsent recipients](#unsent-recipients), below, says why. |
|
|
20
|
+
|
|
21
|
+
A line under the counts accounts for every recipient by name, so the numbers always add up. It also names two kinds the counts leave out. A recipient **accepted, awaiting a delivery receipt** was taken by your email provider, with no word yet on what happened next. A **skipped** recipient unsubscribed or was suppressed before their turn, so they were never mailed.
|
|
22
|
+
|
|
23
|
+
Below that is the full list of recipients. It opens on **Failures** (bounced, complained, and unsent), and **Delivered** and **All** are one click away. You can search it by address. Each row shows the outcome, the provider's own words about it, and when it happened. A person you erased reads as **Erased recipient** in place of their address. **Export recipients (CSV)** downloads the whole list.
|
|
24
|
+
|
|
25
|
+
### Accepted, then settled
|
|
26
|
+
|
|
27
|
+
Delivery happens in two stages, and the record keeps them apart. First Kestrel hands each email to your email provider, which accepts it, usually within minutes. Later, the provider reports what happened to each email: delivered, bounced, or complained. Those reports can lag by seconds or by days.
|
|
28
|
+
|
|
29
|
+
So a send is **sent** once every recipient has been handed off. Its record then keeps updating as reports arrive. The page says **Settling** while it waits, and **Complete** once every accepted email is confirmed. Some accepted emails are never confirmed at all, so a send can stay **Settling** for good. That's normal, not a fault.
|
|
30
|
+
|
|
31
|
+
"Delivered" in Kestrel always means confirmed, never just accepted. So a count can rise for days after a send finishes, and an email can be accepted without ever showing as delivered.
|
|
32
|
+
|
|
33
|
+
## Unsent recipients
|
|
34
|
+
|
|
35
|
+
An unsent recipient never got this post, and is still on your list. They get your next post as usual.
|
|
36
|
+
|
|
37
|
+
A recipient is recorded unsent in two cases:
|
|
38
|
+
|
|
39
|
+
- **Their own email kept failing.** When handing off one address hits a passing error, Kestrel tries again a few times, then records it unsent. The row's detail gives the provider's words.
|
|
40
|
+
- **You chose Assume not sent.** Rarely, Kestrel can't tell whether your provider took some emails. The send waits for you, and **Resolve…** on its page asks you to decide. **Assume not sent** is the safe default: those readers may miss this post, but no one is mailed twice.
|
|
41
|
+
|
|
42
|
+
A problem with your email provider itself never marks anyone unsent. While the provider is down, or refusing your account, the send waits and tries again. Once the problem is fixed, it carries on from where it stopped.
|
|
43
|
+
|
|
44
|
+
## Bounces and complaints
|
|
45
|
+
|
|
46
|
+
A bounce or a complaint usually arrives after the send has finished, and lands on the record when it does. Kestrel acts on it for you, so a bad address can't keep hurting your sending reputation:
|
|
47
|
+
|
|
48
|
+
- **A hard bounce** means the address can't receive mail, such as one that doesn't exist. Kestrel suppresses it, so it's never mailed again.
|
|
49
|
+
- **A soft bounce** is temporary, such as a full mailbox. It's counted on the record, and the address stays on your list.
|
|
50
|
+
- **A complaint** means the reader marked your email as spam. Kestrel suppresses the address, so it's never mailed again.
|
|
51
|
+
|
|
52
|
+
The record's list says **Hard bounce** or **Soft bounce** on each row. A suppression doesn't change anyone's subscription. Under **Subscribers**, a suppressed address keeps its status and carries a label saying why: **bounced**, **complaint**, **blocked**, or **erased**. **Legend**, beside the counts, says what each label means. Choose the **suppressed** count to list only those subscribers. [Sending rules](02-sending-rules.md#what-kestrel-handles) says why this matters to mailbox providers.
|
|
53
|
+
|
|
54
|
+
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 it's worth finding out why.
|
|
55
|
+
|
|
56
|
+
### Lifting a suppression
|
|
57
|
+
|
|
58
|
+
Kestrel never lifts a suppression on its own, and neither does an import. Only you can, deliberately. Do it only when you know the address works again, such as a reader who tells you their mailbox is fixed. Mailing an address that keeps bouncing, or a reader who marked you as spam, can hurt every send after it.
|
|
59
|
+
|
|
60
|
+
A block is yours to lift: **Unblock** in the subscriber's ⋯ menu, which asks you to confirm. It's offered for a block you made, or one an import brought. The dashboard has no control for lifting a bounce or a complaint, but Claude can do it for you through the API, as [Working with Claude](07-working-with-claude.md) describes. Lifting a suppression changes nothing on past records: the bounce or complaint stays where it happened.
|
|
61
|
+
|
|
62
|
+
## Notifications
|
|
63
|
+
|
|
64
|
+
Kestrel emails you when a send finishes, and when a send runs into a problem it can't settle on its own. So you only need to open the dashboard to act.
|
|
65
|
+
|
|
66
|
+
Set the address under **Settings → Notifications**, in **Send notifications to**. **Send a test notification** checks that one arrives. With no address set, Kestrel sends nothing, and doesn't save anything up for later.
|
|
67
|
+
|
|
68
|
+
You're told:
|
|
69
|
+
|
|
70
|
+
- **When a send finishes,** with how many emails your provider accepted, how many were unsent, and how many were skipped.
|
|
71
|
+
- **When your provider refuses your account,** with the provider's own words and what they point to. Amazon SES reports a spent daily quota this way too.
|
|
72
|
+
- **When a send is held because no email provider is connected,** and that it goes out once one is.
|
|
73
|
+
- **When a send stays in progress too long.**
|
|
74
|
+
- **When a send waits for you to resolve it.**
|
|
75
|
+
- **When a send missed its time,** going out late or not at all.
|
|
76
|
+
- **When Claude imports a list for you,** with what the import changed.
|
|
77
|
+
|
|
78
|
+
Each notification links straight to the page it's about. Each event is told once, not again while it lasts, and the dashboard keeps showing it until it clears. A bounce spike is shown on the dashboard, but isn't emailed.
|
|
79
|
+
|
|
80
|
+
The **Last notification** row in the same section says whether the latest one was **Delivered** or **Not delivered**. Notifications are separate from your newsletter: one that fails never delays or changes a send.
|
|
81
|
+
|
|
82
|
+
Whoever runs your Kestrel chooses how notifications are sent. By default they go through your newsletter's own email provider. Then the one about your provider refusing your account can't reach you, since that provider would refuse it too. Whoever runs your Kestrel can send them another way that avoids this, as [Notifications through Cloudflare's email](../guides/03-notifications.md) describes.
|
|
83
|
+
|
|
84
|
+
## Seeing what's happening
|
|
85
|
+
|
|
86
|
+
The **Dashboard** shows what needs you first, then what's going on:
|
|
87
|
+
|
|
88
|
+
- **A health block,** only when something needs your attention, such as a send waiting for you to resolve it. Red needs you to act; amber is worth a look.
|
|
89
|
+
- **Active sends,** a card for each send going out now, with its progress.
|
|
90
|
+
- **Subscribers,** counts of **Confirmed**, **Pending**, **Unsubscribed**, and **Suppressed**. Each opens the list filtered to that group.
|
|
91
|
+
- **Sent,** your five latest sends, each with its **Delivered** count and a link to its public page.
|
|
92
|
+
|
|
93
|
+
**Sent** in the sidebar has the rest: anything that needs attention, your scheduled sends, sends in progress, and every send so far. Search them by subject, or turn on **With delivery failures** to list only sends with a bounce, a complaint, or an unsent recipient.
|
|
94
|
+
|
|
95
|
+
Wherever a send is listed, its number is **Delivered**, with any complaints, bounces, or unsent recipients named beside it. Every page keeps up on its own, so you never need to reload.
|
|
96
|
+
|
|
97
|
+
While a send goes out, its page shows two bars. The first fills as your provider accepts each email, and the second fills behind it as deliveries are confirmed. A label beside the subject says how it's going, such as **Sending**, **Retrying**, or **Backing off**.
|
|
98
|
+
|
|
99
|
+
## The post's public page
|
|
100
|
+
|
|
101
|
+
Your post goes up in your archive the moment its send starts. So the "view in browser" link in every email works as soon as it lands. Its publish date is the day its send started.
|
|
102
|
+
|
|
103
|
+
The page shows the exact copy that was sent, never a fresh rendering of it. Kestrel adds a light frame for the browser: your publication's name, the publish date, a link back to the archive, and a way to subscribe. It also leaves out the parts that only make sense in an inbox, such as the unsubscribe link and your mailing address. No reader's address ever appears on it.
|
|
104
|
+
|
|
105
|
+
A sent post can't be changed, and neither can its page. The sign-off shows your publication as it was when you sent the post, while the frame shows it as it is now. The page's address is permanent, so links to it keep working.
|
|
106
|
+
|
|
107
|
+
Open it from **View published post ↗** on the send's record, or **Archive ↗** in the dashboard's **Sent** list. **View publication ↗** in the sidebar opens your home page, which links into your archive. [Name your publication](03-name-your-publication.md) says where your name, logo, and address appear.
|
|
108
|
+
|
|
109
|
+
Next, see what Claude can do for you through the same API in [Working with Claude](07-working-with-claude.md).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Working with Claude
|
|
2
|
+
|
|
3
|
+
Claude works through the same API as the dashboard, so it can do what you do in the dashboard, and nothing more. It drafts, proofreads, tests, schedules, and reports, and everything it changes shows in the dashboard as Claude's.
|
|
4
|
+
|
|
5
|
+
This page covers what to ask Claude for, the lines the app holds for any client, and the lines Claude keeps on its own. Setting Claude up is a job for whoever runs your Kestrel: [Connect Claude to the API](../guides/01-connect-claude.md) walks through it. Claude reads these same pages from your Kestrel, so it explains things in the words you see here.
|
|
6
|
+
|
|
7
|
+
## What to ask Claude
|
|
8
|
+
|
|
9
|
+
Ask in plain words, the way you'd ask a colleague. Claude reads the API reference, `GET /api/reference`, before its first call, and works out the rest.
|
|
10
|
+
|
|
11
|
+
- **Draft a post.** Give it your notes, an outline, or a topic, and it saves a draft you can open in the dashboard. Images are uploaded to the post and referenced by file name, as [Write your first post](04-write-a-post.md) describes.
|
|
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.
|
|
18
|
+
|
|
19
|
+
If you have a post open when Claude saves it, the editor tells you, and names Claude as the one who changed it.
|
|
20
|
+
|
|
21
|
+
## Lines the app holds for any client
|
|
22
|
+
|
|
23
|
+
The app enforces these itself, for the dashboard and Claude alike, so they hold whatever Claude is told:
|
|
24
|
+
|
|
25
|
+
- **Every send waits out the review window.** Nothing goes out the moment it's asked for, not even a send now, and you can cancel it until it fires. The shortest window is set when Kestrel is deployed, so no client can shorten it through the API.
|
|
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.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Subscriber import format
|
|
2
|
+
|
|
3
|
+
Kestrel imports a subscriber list in one documented format, `kestrel-subscribers/1`, and exports the whole list in the same format. This page describes the format, what an import does to each address, and the limits.
|
|
4
|
+
|
|
5
|
+
Kestrel never guesses what another service's export means, because a wrong guess about who agreed to be emailed can't be taken back. An export from Sendy, Mailchimp, or anywhere else is converted to this format first, by Claude or by hand. The API serves everything on this page at `GET /api/imports/format`, generated from the same definition the importer reads with, along with an example of each kind of file, `kestrel-import-example.csv` at `GET /api/imports/template.csv` and `kestrel-import-example.txt` at `GET /api/imports/template.txt`. The import screen offers both as **Download example file**.
|
|
6
|
+
|
|
7
|
+
## Two kinds of file
|
|
8
|
+
|
|
9
|
+
- **The full CSV (preferred).** It carries unsubscribes, bounces, and how each person joined. It's what the export writes, and you can open it in a spreadsheet to check it before importing.
|
|
10
|
+
- **A plain list (`.txt`).** One address per line, for a list that is only addresses. You say what they all are: subscribed (the default), unsubscribed, or bounced. Nothing else carries over: not how or when each person joined, when anyone left, or any evidence of their consent. And since every address is the one kind you choose, unsubscribes and bounces each need a file of their own.
|
|
11
|
+
|
|
12
|
+
The file's name decides which it is: `.csv` or `.txt`. Anything else is refused.
|
|
13
|
+
|
|
14
|
+
## The CSV
|
|
15
|
+
|
|
16
|
+
A CSV file saved as UTF-8 (in a spreadsheet, **CSV UTF-8**), with a header row of the column names below, in any order, then one row per address. A value with a comma, a quote, or a line break in it is wrapped in double quotes, with each quote inside written twice, as spreadsheets write it (RFC 4180). Column names are written exactly, in lowercase. A row records what the source claims, never what Kestrel will do: the import decides that, under the rules further down.
|
|
17
|
+
|
|
18
|
+
| Column | Required | Values | Meaning |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| `email` | yes | an email address | The address, trimmed and lowercased before it is matched. An invalid one is still a row, counted as invalid and reported, never dropped. |
|
|
21
|
+
| `status` | yes | `subscribed`, `pending`, `unsubscribed`, `none` | The consent claim. `none` is a known address with no consent claim, present only to carry a suppression. |
|
|
22
|
+
| `opt_in` | yes | `double`, `single`, `unknown` | How consent was collected. `unknown` unless the source states it. |
|
|
23
|
+
| `origin` | yes | a short lowercase name | Where consent was first collected, as the file claims it: `sendy`, `mailchimp`, `kestrel`, or `unknown`. |
|
|
24
|
+
| `suppression` | no | `bounce`, `complaint`, `manual`, `erased` | A suppression on top of `status`: the address bounced, complained, was suppressed by hand, or was erased. |
|
|
25
|
+
| `subscribed_at` | no | an instant with its zone | When the person subscribed, if the source states it. |
|
|
26
|
+
| `confirmed_at` | no | an instant with its zone | When the person confirmed, if the source states it. |
|
|
27
|
+
| `unsubscribed_at` | no | an instant with its zone | When the person left, if the source states it. A reader who confirmed in Kestrel after it stays confirmed. |
|
|
28
|
+
| `suppressed_at` | no | an instant with its zone | When the address was suppressed, if the source states it. |
|
|
29
|
+
| `evidence` | no | a JSON object, at most 500 bytes | The source's consent-relevant columns, verbatim, such as a signup IP or a form name. Nothing reads it to decide anything: it is the proof of consent and the audit trail of a conversion. |
|
|
30
|
+
| `format` | no | `kestrel-subscribers/1` | The version the file is written in. A file without it is read as version 1. |
|
|
31
|
+
|
|
32
|
+
An instant is a UTC ISO-8601 time with its zone, such as `2024-03-01T09:30:00Z` or `2024-03-01T10:30:00+01:00`. A time without a zone is refused, never interpreted. An optional column may be left out, or left empty on a row.
|
|
33
|
+
|
|
34
|
+
Any other column is refused, naming it. A source's other columns belong in `evidence`, as JSON. A file whose header names none of these columns isn't in the format at all, and is refused as such.
|
|
35
|
+
|
|
36
|
+
The example file, `kestrel-import-example.csv`, shows each case: a double opt-in subscriber with evidence, a single opt-in one, someone who left, and a bounced address. Here are the same four with only the columns they use, since an optional column may be left out:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
email,status,opt_in,origin,suppression,unsubscribed_at,evidence
|
|
40
|
+
ada@example.com,subscribed,double,sendy,,,"{""signup_ip"":""203.0.113.7""}"
|
|
41
|
+
grace@example.com,subscribed,single,mailchimp,,,
|
|
42
|
+
edsger@example.com,unsubscribed,unknown,unknown,,2025-01-20T08:00:00Z,
|
|
43
|
+
bounced@example.com,none,unknown,unknown,bounce,,
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Versions.** Adding an optional column keeps the version. Changing what a column means makes a new version. Kestrel reads every version it has published, and refuses one it doesn't know, by name.
|
|
47
|
+
|
|
48
|
+
## The plain list
|
|
49
|
+
|
|
50
|
+
- One address per line. Each line is trimmed, blank lines are skipped, and CRLF line endings and a UTF-8 byte-order mark are accepted.
|
|
51
|
+
- A line that isn't a single valid address, such as `Name <ada@example.com>` or a typo, is counted as invalid and listed among the rows left out. One bad line doesn't sink a list.
|
|
52
|
+
- The whole file is refused when it isn't a plain list at all: not UTF-8 text, a line with a comma, tab, or semicolon (it's a table, so a `.csv`), or not one valid address in it.
|
|
53
|
+
- Its rows arrive with `origin: unknown` and `opt_in: unknown`.
|
|
54
|
+
|
|
55
|
+
## Plan, then commit
|
|
56
|
+
|
|
57
|
+
An import happens in two steps. Planning shows what the import would do to every row, against your list as it stands, and changes nothing, so you can plan a file as often as you need. Committing applies the plan, all of it or none of it, and sends no email.
|
|
58
|
+
|
|
59
|
+
The commit needs two things from you when they apply:
|
|
60
|
+
|
|
61
|
+
- **Your attestation,** when anyone would be confirmed: "Everyone I'm importing as subscribed opted in to receive [your publication]. This isn't a purchased, rented, or scraped list." It is kept with the import, with who gave it and when. An import that only unsubscribes or suppresses needs none.
|
|
62
|
+
- **Each send that would grow,** acknowledged by a person. A post goes to the list as it stands when it fires, so an import can add to the audience of a scheduled send, or of one that has just fired and not yet fixed its audience. Claude can attest for you, but can't acknowledge a growing send: you finish that import in the dashboard.
|
|
63
|
+
|
|
64
|
+
If another import is committed after you plan, plan again. If your list changes between the plan and the commit in any other way, such as a reader unsubscribing, the commit still goes ahead. Every write checks the list as it stands, and the answer reports any row that turned out differently.
|
|
65
|
+
|
|
66
|
+
## What an import does to each address
|
|
67
|
+
|
|
68
|
+
Each row gets exactly one outcome. A row that both changes its status and adds a suppression reports its status outcome, and says it added a suppression too.
|
|
69
|
+
|
|
70
|
+
| Outcome | When |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `create_confirmed` | `subscribed`, with no subscriber and no suppression in Kestrel, and not erased in the file: added as confirmed. |
|
|
73
|
+
| `confirm_pending` | `subscribed`, pending in Kestrel (never confirmed), and not erased in the file: confirmed, and its confirmation link no longer works. |
|
|
74
|
+
| `unchanged` | Already confirmed in Kestrel, or already in the state the row claims. A subscriber who joined through Kestrel keeps their own record. |
|
|
75
|
+
| `mark_unsubscribed` | `unsubscribed`, and confirmed or pending in Kestrel, with no confirmation in Kestrel newer than the row's `unsubscribed_at`: unsubscribed. |
|
|
76
|
+
| `create_unsubscribed` | `unsubscribed`, with no subscriber in Kestrel, and not erased in Kestrel or in the file: recorded as unsubscribed, so the list remembers they left. |
|
|
77
|
+
| `kept_confirmed_newer` | `unsubscribed`, but the reader confirmed in Kestrel after the source's `unsubscribed_at`: their newer action stands. |
|
|
78
|
+
| `kept_unsubscribed` | `subscribed`, but unsubscribed in Kestrel, or pending again after leaving until they confirm in Kestrel themselves. An unsubscribe is never reversed. |
|
|
79
|
+
| `blocked_by_suppression` | `subscribed`, but the address carries a suppression in Kestrel, a do-not-contact marker included: never added or confirmed. Also `unsubscribed` for an address whose suppression in Kestrel is the do-not-contact marker an erasure left, and, for an address erased in the file (its `suppression` is `erased`, which is still added), `subscribed` or `unsubscribed` with no subscriber in Kestrel, or `subscribed` and pending in Kestrel, which stays pending: no subscriber is recorded or confirmed, so an import never brings back an erased person's data. |
|
|
80
|
+
| `suppression_added` | The row carries a suppression and the address has none in Kestrel, or only a do-not-contact marker, and nothing else changes. Applied whatever the row's `status`. |
|
|
81
|
+
| `left_out` | `pending` or `none` with nothing to apply: counted, never confirmed. An import sends no email, so it can't start a confirmation either. |
|
|
82
|
+
| `invalid` | The address isn't valid. |
|
|
83
|
+
| `merged` | A repeat of an address earlier in the file, folded into it. |
|
|
84
|
+
|
|
85
|
+
A suppression already in Kestrel is never replaced, with one exception: a bounce, a complaint, or a manual suppression replaces a do-not-contact marker left by an erasure, so a known bad address still gets no confirmation. An imported bounce or complaint is kept apart from one Kestrel saw itself, so the list says where each came from.
|
|
86
|
+
|
|
87
|
+
Anyone can type an address into your subscribe form, which puts a reader who left back to pending until they confirm. An import treats such a reader as unsubscribed, so it never confirms someone who left in Kestrel.
|
|
88
|
+
|
|
89
|
+
**Repeats of an address** are folded into one row first. Its status is the first of unsubscribed, subscribed, pending, and none that any repeat claims, and its suppression the first of erased, complaint, bounce, and manual. Its opt-in is the weakest claim. It keeps the earliest consent times and the latest unsubscribe and suppression times, and the origin and evidence of the row whose status won.
|
|
90
|
+
|
|
91
|
+
## How imported subscribers read
|
|
92
|
+
|
|
93
|
+
A subscriber an import confirmed is **confirmed**, like anyone who confirmed in Kestrel, and gets your posts. How they joined shows beside their state: imported, with the origin and opt-in method the file claimed, and the evidence. That holds even when the file says they confirmed through Kestrel itself. Only a confirmation Kestrel recorded reads as Kestrel's double opt-in, and a subscriber who later confirms in Kestrel becomes Kestrel's own.
|
|
94
|
+
|
|
95
|
+
## Limits
|
|
96
|
+
|
|
97
|
+
- **Rows:** up to 10,000 in one import. On the Workers Free plan's limits (`SUBREQUEST_BUDGET` unset), up to 1,000, since Free's 10 ms of CPU a request can't plan and commit more. Set [`SUBREQUEST_BUDGET`](../reference/01-configuration.md#subrequest_budget) on Workers Paid to take 10,000. Split a larger list into several files.
|
|
98
|
+
- **Size:** a request of at most 10 MB, and `evidence` of at most 500 bytes a row.
|
|
99
|
+
- **Database writes on Workers Free:** D1's free plan allows 100,000 rows written a day, and refuses every query past that until midnight UTC. Each new subscriber an import brings in writes 7 rows, and a send writes about 14 a recipient. So one import of 1,000 new subscribers uses about 7% of a day's writes, and about 10 of them use most of a day. On Workers Free, the plan warns when it and the day's earlier imports, by UTC, would use more than 70% of the day, and advises importing the file the next day. A send scheduled for a day imports have written warns in the same way, and advises sending the next day. Neither warning refuses.
|
|
100
|
+
|
|
101
|
+
## Exporting
|
|
102
|
+
|
|
103
|
+
The export reads your whole list out in this format, a page at a time, at `GET /api/subscribers/export`. Each page holds up to 1,000 rows on the Workers Free plan's limits, or 10,000 otherwise, and its `next` value asks for the page after it. Following `next` until it's `null` gives every address exactly once, even if the list changes while you read. An address added or removed partway through may or may not appear. To make one CSV in this format, write the pages' rows under a single header. The dashboard's **Export** button, on the Subscribers screen, does this for you and downloads the whole list as one CSV. In the CSV, a value that starts with `=`, `+`, `-`, or `@` is written with an apostrophe before it, so a spreadsheet shows it as text rather than running it as a formula; an import takes the apostrophe off again. It includes every subscriber with how they joined, their evidence, and their suppression, and every address suppressed with no subscriber behind it, written as `status: none`. A confirmed subscriber is written as `subscribed`. Importing an export into another Kestrel carries the list over: the same states and suppressions, with every confirmation arriving as imported, carrying its original evidence. Pending subscribers are left out on the way in, since an import sends no confirmation.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Share your subscribe link
|
|
2
|
+
|
|
3
|
+
Your publication has a public subscribe page at `/subscribe` on your Kestrel's address, such as `https://newsletter.example.com/subscribe`. Share that link wherever your readers are, or put the same form on your own website. Either way, a reader joins only once they confirm by email.
|
|
4
|
+
|
|
5
|
+
This page covers what a reader sees, the confirmation email, putting the form on your site, and previewing every page a reader can land on. You find the link under **Settings → Ways to subscribe**, and on the dashboard's first-run checklist.
|
|
6
|
+
|
|
7
|
+
## What a reader sees
|
|
8
|
+
|
|
9
|
+
**A reader enters their address, gets a confirmation email, and presses a button to confirm. Only then do they receive your posts.**
|
|
10
|
+
|
|
11
|
+
The subscribe page wears your publication's name, tagline, and logo, which you set in [Name your publication](03-name-your-publication.md). Your landing page and archive carry a subscribe prompt too. After a reader submits their address, the page tells them to check their inbox. The email's link opens a page with a **Confirm subscription** button, and pressing it puts them on your list. This is double opt-in, and it's your record that each reader asked for your newsletter.
|
|
12
|
+
|
|
13
|
+
Opening the link alone records nothing, since mail scanners open links before a person does. A confirmation link also works only for a limited time. An expired link offers to send a fresh one.
|
|
14
|
+
|
|
15
|
+
## Addresses already on your list, or that left
|
|
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.
|
|
26
|
+
|
|
27
|
+
## The confirmation email
|
|
28
|
+
|
|
29
|
+
**You can change its words in Settings → Confirmation email. Its layout and its link are fixed.**
|
|
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.
|
|
41
|
+
|
|
42
|
+
## Put the form on your own site
|
|
43
|
+
|
|
44
|
+
**Copy the form from Settings → Ways to subscribe → Embed a form, and paste it into any page you control.**
|
|
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.
|
|
72
|
+
|
|
73
|
+
Next, send your first post: [Scheduling and sending](05-scheduling-and-sending.md).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Check your setup
|
|
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. It's the second step of **Set up your publication** on your dashboard.
|
|
4
|
+
|
|
5
|
+
This page covers whether email is connected, what to check in a test, how Kestrel tells you something's wrong, and what changes as your list grows.
|
|
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).
|
|
12
|
+
|
|
13
|
+
## Send yourself a test
|
|
14
|
+
|
|
15
|
+
A test shows you exactly what readers get, and you can send one before you write any post. Choose **Template** in the sidebar, then **Send test email**, and send a sample post to your own inboxes. [Name your publication](03-name-your-publication.md) covers the template, and [Write your first post](04-write-a-post.md#test-emails) covers what a test is.
|
|
16
|
+
|
|
17
|
+
When it arrives, look at:
|
|
18
|
+
|
|
19
|
+
- **The From name and address.** They're what readers see first.
|
|
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).
|
|
23
|
+
|
|
24
|
+
## Subscribe yourself
|
|
25
|
+
|
|
26
|
+
Subscribing with a personal address shows you what a new reader sees. Use your public subscribe page, at your publication's address followed by `/subscribe`. The confirmation email arrives with your wording. Once you confirm, you appear under **Subscribers** as **Confirmed**. [Share your subscribe link](09-share-your-subscribe-link.md) covers the page and the confirmation email.
|
|
27
|
+
|
|
28
|
+
## How Kestrel tells you something's wrong
|
|
29
|
+
|
|
30
|
+
Kestrel tells you in three places: your inbox, the dashboard, and the Schedule dialog. Most of the time, none of them says anything.
|
|
31
|
+
|
|
32
|
+
- **Notifications.** Set your address under **Settings → Notifications**, in **Send notifications to**, and save. **Send a test notification** checks that one arrives. With no address set, nothing is emailed. [After a send](06-after-a-send.md#notifications) lists what you're told.
|
|
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.
|
|
35
|
+
|
|
36
|
+
Whoever runs your Kestrel chooses how notifications are sent. [Notifications through Cloudflare's email](../guides/03-notifications.md) covers a way that reaches you even when your email provider refuses your account.
|
|
37
|
+
|
|
38
|
+
## As your list grows
|
|
39
|
+
|
|
40
|
+
A growing list meets a few limits, and Kestrel tells you before or when it reaches each one. Raising each is a change for whoever runs your Kestrel, on the plans and accounts they chose, as [Go live](../get-started/06-go-live.md#as-your-list-grows) describes.
|
|
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.
|
|
47
|
+
|
|
48
|
+
Next, [Bring your existing list](01-your-list.md).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Publishing with Kestrel
|
|
2
|
+
|
|
3
|
+
These pages cover writing and sending your newsletter in the dashboard, from naming your publication to what happens after each send. Deploying and running Kestrel itself is covered in [Self-hosting](../README.md).
|
|
4
|
+
|
|
5
|
+
## Set up your publication
|
|
6
|
+
|
|
7
|
+
The steps your dashboard lists until your first post, in the same order.
|
|
8
|
+
|
|
9
|
+
1. [Name your publication](03-name-your-publication.md)
|
|
10
|
+
1. [Check your setup](10-check-your-setup.md)
|
|
11
|
+
1. [Bring your existing list](01-your-list.md)
|
|
12
|
+
1. [Write your first post](04-write-a-post.md)
|
|
13
|
+
1. [Share your subscribe link](09-share-your-subscribe-link.md)
|
|
14
|
+
|
|
15
|
+
## Sending
|
|
16
|
+
|
|
17
|
+
Scheduling a post, what happens when it goes out, and the rules every send keeps.
|
|
18
|
+
|
|
19
|
+
- [Scheduling and sending](05-scheduling-and-sending.md)
|
|
20
|
+
- [After a send](06-after-a-send.md)
|
|
21
|
+
- [Sending rules](02-sending-rules.md)
|
|
22
|
+
|
|
23
|
+
## More
|
|
24
|
+
|
|
25
|
+
- [Working with Claude](07-working-with-claude.md)
|
|
26
|
+
- [Subscriber import format](08-import-format.md)
|