@kurtbruns/kestrel 1.9.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/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Set up and run Kestrel
1
+ # Self-hosting Kestrel
2
2
 
3
- These guides deploy and run your own instance of Kestrel, from an empty repository to a newsletter on your own Cloudflare account.
3
+ These pages deploy and run your own instance of Kestrel, from an empty repository to a newsletter on your own Cloudflare account. Writing and sending the newsletter is covered in [Publishing](publishing/README.md).
4
4
 
5
5
  ## Get started
6
6
 
@@ -27,5 +27,4 @@ Each one stands on its own. Pick the ones you need.
27
27
  The detail behind the steps, for when you need it.
28
28
 
29
29
  - [Configuration](reference/01-configuration.md)
30
- - [Sending-domain DNS](reference/02-sending-domain-dns.md)
31
- - [Import format](reference/03-import-format.md)
30
+ - [Sending-domain DNS: SPF, DKIM, and DMARC](reference/02-sending-domain-dns.md)
@@ -2,7 +2,7 @@
2
2
 
3
3
  Kestrel sends your posts through Amazon SES, Amazon's email service. SES only delivers the mail: Kestrel keeps your list, each subscriber's consent, and the record of every send. AWS reviews your account before it lets you send to anyone but yourself. This page asks for that early, and you test in the sandbox while you wait.
4
4
 
5
- In this guide, you verify your sending hostname with SES, add a DMARC record, and ask AWS for production access. You also route bounces and complaints to the app through SNS. Then you give the app its AWS credentials, set the provider, and deploy.
5
+ In this guide, you verify your sending hostname with SES and add a DMARC record. You forward readers' replies to your inbox, and ask AWS for production access. You also route bounces and complaints to the app through SNS. Then you give the app its AWS credentials, set the provider, and deploy.
6
6
 
7
7
  ## Before you begin
8
8
 
@@ -42,7 +42,33 @@ DMARC tells inboxes what to do with mail that fails its checks, and sends you re
42
42
 
43
43
  [Sending-domain DNS](../reference/02-sending-domain-dns.md#dmarc-publish-a-policy-and-collect-reports) explains each record, and when to tighten DMARC.
44
44
 
45
- ## 3. (Optional) Set a custom MAIL FROM domain
45
+ ## 3. Receive replies at your From address
46
+
47
+ When a reader replies to a post, or to their confirmation email, the reply goes to your From address, `newsletter@send.example.com`. Nothing receives mail there yet, so the reply bounces. Kestrel only sends mail, so you give the address an inbox yourself. Here you forward it to one you already read, with Cloudflare Email Routing, which is free on every plan.
48
+
49
+ `send.example.com` receives no other mail, so turning on Email Routing there changes nothing else. Your SES records sit on other names under it. Inboxes check SES's mail for SPF at its return path, not here, so sending is unaffected.
50
+
51
+ 1. In the [Cloudflare dashboard](https://dash.cloudflare.com/), go to **Compute → Email Service → Email Routing**.
52
+
53
+ 1. Add `send.example.com` to Email Routing. Cloudflare adds subdomains from your domain's own settings: select `example.com`, then **Settings**, and add `send` under **Subdomains**.
54
+
55
+ 1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added.
56
+
57
+ 1. Under **Destination Addresses**, add the inbox you want replies in. Open the email Cloudflare sends to it, and select **Verify email address**. An address you've already verified with Cloudflare, such as one for [notifications](../guides/03-notifications.md), is listed already.
58
+
59
+ 1. Under **Routing Rules**, select **Create routing rule**. For **Email pattern**, enter `newsletter`, the part of your From address before the `@`, and select `send.example.com`. Set **Action** to **Send to an email**, select your inbox as the **Destination**, and save.
60
+
61
+ On a subdomain, Email Routing forwards only the addresses you add a rule for, with no catch-all. That's all a From address needs.
62
+
63
+ - **Automatic replies arrive too.** Out-of-office and other automatic replies go to your From address, so they reach your inbox. Bounces don't: they go to SES's return path, and the app handles them as before.
64
+ - **Your DMARC reports need an inbox too.** Your DMARC record's report address may not receive mail. If so, change the record's `rua` to `mailto:dmarc@send.example.com`, and add a second rule that forwards `dmarc` the same way.
65
+ - **Your answers come from your inbox.** A forwarded reply comes from the reader's own address. When you answer it, your answer goes out from the inbox it was forwarded to, not from `newsletter@send.example.com`.
66
+
67
+ To answer from the newsletter's own address, give it a real mailbox instead of forwarding it. Point `send.example.com`'s MX records at a mail host you use, such as [Fastmail](https://www.fastmail.help/hc/en-us/articles/360060591133) or [Google Workspace](https://support.google.com/a/answer/140034). Then create `newsletter@send.example.com` there, as the host's docs describe. If you send from it, the host's SPF and DKIM records go on `send.example.com` beside SES's. SES's mail is unaffected, since its SPF is checked at its return path. And if your From address is on a domain that already receives mail, replies already reach it, and you can skip this section.
68
+
69
+ Some replies ask you to do something, and Kestrel can't see them, so acting on them is yours. In the editor, open **Subscribers**, and use the reader's row menu. Choose **Unsubscribe** for a reader who asks to leave, and **Erase** for one who asks you to delete their data. A reader who never confirmed isn't mailed, so their row offers only **Erase**.
70
+
71
+ ## 4. (Optional) Set a custom MAIL FROM domain
46
72
 
47
73
  SES sends with its own return path by default, so SPF passes for `amazonses.com` rather than for your hostname. DMARC still passes, through DKIM. A custom MAIL FROM domain makes SPF pass for your hostname too, a second check in your favor.
48
74
 
@@ -57,11 +83,11 @@ SES sends with its own return path by default, so SPF passes for `amazonses.com`
57
83
 
58
84
  1. Wait until SES shows the MAIL FROM domain as **Successful**.
59
85
 
60
- ## 4. Request production access
86
+ ## 5. Request production access
61
87
 
62
88
  A new SES account starts in the sandbox. It sends only to addresses you've verified in SES, up to 200 emails a day and one a second. Ask AWS to lift those limits now, and test while you wait.
63
89
 
64
- 1. Name your publication first, since AWS may visit your website to review the request. In the editor, open **Settings → Publication identity**, and enter the name and tagline. Until you do, your public pages show the name in your From address. The subscribe form stays closed until section 7 sets the provider.
90
+ 1. Name your publication first, since AWS may visit your website to review the request. In the editor, open **Settings → Publication identity**, and enter the name and tagline. Until you do, your public pages show the name in your From address. The subscribe form stays closed until section 8 sets the provider.
65
91
 
66
92
  1. In SES, open the **Account dashboard**, select **View Get set up page**, and then **Request production access**.
67
93
 
@@ -83,9 +109,9 @@ If AWS asks for more detail, reply in the support case. If it denies the request
83
109
 
84
110
  Your account stays in the sandbox until AWS grants access, so don't go live before then. In the sandbox, a signup from any address you haven't verified fails, and a post reaches only your verified addresses.
85
111
 
86
- When AWS grants access, the **Account dashboard** shows your daily quota and your maximum send rate. You set the rate in the app in section 7. The rate decides how fast a send goes out, and [As your list grows](06-go-live.md#as-your-list-grows) says when to ask AWS to raise it.
112
+ When AWS grants access, the **Account dashboard** shows your daily quota and your maximum send rate. You set the rate in the app in section 8. The rate decides how fast a send goes out, and [As your list grows](06-go-live.md#as-your-list-grows) says when to ask AWS to raise it.
87
113
 
88
- ## 5. Send bounces and complaints to SNS
114
+ ## 6. Send bounces and complaints to SNS
89
115
 
90
116
  SES reports what happens to each email through SNS, the AWS notification service. A configuration set says which events to report, and an SNS topic carries them to the app.
91
117
 
@@ -120,9 +146,9 @@ SES reports what happens to each email through SNS, the AWS notification service
120
146
 
121
147
  Replace the topic ARN, and both copies of the account ID, with the values from your ARN. Replace `us-east-1` with your region's code, the same one the ARN shows. Choose **Save changes**.
122
148
 
123
- You subscribe the app to the topic in section 8, once it runs on SES.
149
+ You subscribe the app to the topic in section 9, once it runs on SES.
124
150
 
125
- ## 6. Create the app's AWS key
151
+ ## 7. Create the app's AWS key
126
152
 
127
153
  The app sends through an IAM user whose key can only send email. That limits the harm if the key ever leaks.
128
154
 
@@ -155,7 +181,7 @@ The app sends through an IAM user whose key can only send email. That limits the
155
181
 
156
182
  1. Copy the access key ID and the secret access key. AWS shows the secret only once.
157
183
 
158
- ## 7. Set the provider and deploy
184
+ ## 8. Set the provider and deploy
159
185
 
160
186
  Until now the app has run with no email provider connected, refusing to send. Setting `PROVIDER` connects SES. Store the secrets before you set it. With `PROVIDER` set to `ses` and a required secret missing, the app refuses to run, and sends nothing, until you store it. `/health` and the editor's **A setting needs fixing** page name the missing secret, and reader pages say the newsletter is temporarily unavailable. `SES_CONFIGURATION_SET` is the one optional secret.
161
187
 
@@ -171,7 +197,7 @@ Until now the app has run with no email provider connected, refusing to send. Se
171
197
  npx wrangler secret put AWS_SECRET_ACCESS_KEY --env production
172
198
  ```
173
199
 
174
- 1. Store the topic ARN from section 5. The app accepts events only from this topic:
200
+ 1. Store the topic ARN from section 6. The app accepts events only from this topic:
175
201
 
176
202
  ```bash
177
203
  npx wrangler secret put SNS_TOPIC_ARN --env production
@@ -214,7 +240,7 @@ Until now the app has run with no email provider connected, refusing to send. Se
214
240
  npx wrangler deploy --env production
215
241
  ```
216
242
 
217
- ## 8. Subscribe the app to the topic
243
+ ## 9. Subscribe the app to the topic
218
244
 
219
245
  SNS delivers events only to a subscriber that has confirmed. The app confirms on its own, once it checks that the request came from your topic.
220
246
 
@@ -234,7 +260,7 @@ If it stays **Pending confirmation**, the request never reached the app, or the
234
260
 
235
261
  - **Access covers `/webhooks/ses`.** If your Access application covers more than `/dashboard` and `/api`, SNS gets the login page instead of the app. Remove that path from your Access application, so only `dashboard` and `api` remain.
236
262
  - **`SNS_TOPIC_ARN` matches the topic's ARN.**
237
- - **The deploy in section 7 finished.**
263
+ - **The deploy in section 8 finished.**
238
264
 
239
265
  Once you've fixed the cause, select the subscription, and choose **Request confirmation**. SNS sends the request again, and the app confirms it. SNS doesn't let you delete a subscription while it's pending, and removes it on its own after 48 hours.
240
266
 
@@ -272,11 +298,25 @@ Kestrel can't tell whether those emails went out. SES's console shows sent messa
272
298
 
273
299
  1. The SNS subscription shows **Confirmed**.
274
300
 
301
+ 1. Your From address has somewhere to go. In Cloudflare, your domain's DNS records show the ones Email Routing added for `send`, beside section 1's DKIM records:
302
+
303
+ - **Type:** `MX`, **Name:** `send`, three of them, for `route1.mx.cloudflare.net`, `route2.mx.cloudflare.net`, and `route3.mx.cloudflare.net`
304
+ - **Type:** `TXT`, **Name:** `send`, **Content:** `v=spf1 include:_spf.mx.cloudflare.net ~all`
305
+ - **Type:** `TXT`, **Name:** `cf2024-1._domainkey.send`, Cloudflare's DKIM key
306
+
307
+ The world sees the MX records too:
308
+
309
+ ```bash
310
+ dig +short MX send.example.com
311
+ ```
312
+
313
+ It lists the three `route` servers. Email Routing's **Routing Rules** shows your rule for `newsletter@send.example.com`, and **Destination Addresses** shows your inbox as verified. A reply to your test email in the next step proves the forwarding works.
314
+
275
315
  1. In the editor, **Settings → Email sender** shows Amazon SES and your From address, where it said **No email provider connected** before.
276
316
 
277
317
  1. In a browser, `https://newsletter.example.com/subscribe` shows the subscribe form, where it said signups aren't open yet.
278
318
 
279
- The test sends in the next step prove the rest. In the sandbox, send them only to the addresses you verified in section 4, and to the SES test addresses that page lists.
319
+ The test sends in the next step prove the rest. In the sandbox, send them only to the addresses you verified in section 5, and to the SES test addresses that page lists.
280
320
 
281
321
  SES charges for each email it sends, test emails included. [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists the rates.
282
322
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  Some things only show up with real email: whether inboxes trust your mail, how it looks in a real mail client, and whether bounces and unsubscribes come back to the app. Check them now, while your list is empty and every email goes only to you.
4
4
 
5
- In this guide, you send yourself a test email and check that it passes authentication. Then you set up your notifications, send a real post to yourself as a subscriber, and unsubscribe. Finally, you check that bounces and complaints reach the app, and read the app's logs.
5
+ In this guide, you send yourself a test email, reply to it, and check that it passes authentication. Then you set up your notifications, send a real post to yourself as a subscriber, and unsubscribe. Finally, you check that bounces and complaints reach the app, and read the app's logs.
6
6
 
7
7
  ## Before you begin
8
8
 
9
- - Two email addresses you own, such as a personal address and a work address. Some steps use both. While your SES account is in the sandbox, verify both in SES first, as [Connect Amazon SES](04-ses.md#4-request-production-access) describes.
9
+ - Two email addresses you own, such as a personal address and a work address. Some steps use both. While your SES account is in the sandbox, verify both in SES first, as [Connect Amazon SES](04-ses.md#5-request-production-access) describes.
10
10
  - A Gmail address, or another mail client that shows a message's original source and an **Unsubscribe** button.
11
11
 
12
12
  ## 1. Send yourself a test email
@@ -19,6 +19,8 @@ A test email goes through the same code as a real send. If the test looks right,
19
19
 
20
20
  1. Open the email. It should arrive in your inbox, not in spam. The image loads and the subject reads as you wrote it. Its **View in browser** link opens "Nothing here yet" until the post goes out, and its unsubscribe link opens the page a subscriber sees, marked as a test and showing a sample address. Its button walks through to the confirmation, but a test email unsubscribes no one. Section 5 unsubscribes for real.
21
21
 
22
+ 1. Reply to it, as a reader would. Reply from an address other than the inbox your From address forwards to, such as your second address. Gmail doesn't show you a message you sent when it comes back to you. Send that address a test email too if you need one. The reply arrives in the inbox you chose in [Connect Amazon SES](04-ses.md#3-receive-replies-at-your-from-address). The app can't see your forwarding, so this is the only proof that readers can reach you. If the reply bounces, check the routing rule and the destination address there.
23
+
22
24
  ## 2. Check that it passes authentication
23
25
 
24
26
  Inboxes trust mail that passes DKIM and DMARC for the domain it's from. Passing both is the best single sign that your mail reaches the inbox.
@@ -80,7 +82,7 @@ When an address bounces or marks your mail as spam, SES tells the app through SN
80
82
 
81
83
  1. In the editor, **Subscribers** lists both addresses as **Pending**, each with a **Suppressed** flag. The app never mails a suppressed address again.
82
84
 
83
- If an address has no flag, SES's events aren't reaching the app. Check the configuration set and the SNS subscription in [Connect Amazon SES](04-ses.md#5-send-bounces-and-complaints-to-sns).
85
+ If an address has no flag, SES's events aren't reaching the app. Check the configuration set and the SNS subscription in [Connect Amazon SES](04-ses.md#6-send-bounces-and-complaints-to-sns).
84
86
 
85
87
  ## 7. Read the logs
86
88
 
@@ -6,14 +6,14 @@ In this guide, you check your publication's name, add a logo, and share your sub
6
6
 
7
7
  ## Before you begin
8
8
 
9
- - SES production access, granted in the region the app sends from. [Connect Amazon SES](04-ses.md#4-request-production-access) asks for it.
10
- - `SES_MAX_SEND_RATE` set to the maximum send rate on the SES **Account dashboard**, and deployed. [Connect Amazon SES](04-ses.md#7-set-the-provider-and-deploy) sets it to `1` for the sandbox.
9
+ - SES production access, granted in the region the app sends from. [Connect Amazon SES](04-ses.md#5-request-production-access) asks for it.
10
+ - `SES_MAX_SEND_RATE` set to the maximum send rate on the SES **Account dashboard**, and deployed. [Connect Amazon SES](04-ses.md#8-set-the-provider-and-deploy) sets it to `1` for the sandbox.
11
11
 
12
12
  In the sandbox, SES refuses the confirmation email to any address you haven't verified, so those signups fail. A post reaches only the addresses you verified in SES.
13
13
 
14
14
  ## 1. Name your publication
15
15
 
16
- Your publication's name, tagline, and logo appear on the public pages readers see, and in every email. You named it in [Connect Amazon SES](04-ses.md#4-request-production-access). Check those, and add a logo before anyone sees your pages.
16
+ Your publication's name, tagline, and logo appear on the public pages readers see, and in every email. You named it in [Connect Amazon SES](04-ses.md#5-request-production-access). Check those, and add a logo before anyone sees your pages.
17
17
 
18
18
  1. In the editor, open **Settings → Publication identity**.
19
19
 
@@ -41,7 +41,7 @@ A new instance runs on Cloudflare's free Workers plan. As your list grows, three
41
41
  - **How fast a send goes out.** A send goes out a slice at a time, one slice a minute. SES takes one request a second, of up to 50 recipients, and no more recipients a second than your account's maximum send rate. So each request carries one second of that rate, up to 50.
42
42
  - **Your database's daily writes.** On the free plan, D1, the database the app runs on, allows 100,000 rows written a day. Past that, it refuses every query until midnight UTC, and the whole app stops: sends, signups, the public pages, and the editor.
43
43
 
44
- It's recommended to move to the $5 Workers Paid plan from about 5,000 subscribers. A send writes about 14 database rows a recipient, delivery receipts included. So one send uses up the free plan's day at about 7,000 recipients, and sooner when an import or anything else writes that day. Workers Paid counts [D1's writes](https://developers.cloudflare.com/d1/platform/pricing/) by the month instead, with 50 million included, and never stops the database for them. On the free plan, the app warns you before a send to more than 5,000 recipients, in the Schedule dialog and on the scheduled post. It never stops you, since your account may already be on Workers Paid.
44
+ It's recommended to move to the $5 Workers Paid plan from about 5,000 subscribers. A send writes about 14 database rows a recipient, delivery receipts included. So one send uses up the free plan's day at about 7,000 recipients, and sooner when an import or anything else writes that day. Workers Paid counts [D1's writes](https://developers.cloudflare.com/d1/platform/pricing/) by the month instead, with 50 million included, and never stops the database for them. On the free plan, the app warns you before a send to more than 5,000 recipients, in the Schedule dialog and on the scheduled post. A smaller send that fires on a day you've imported subscribers gets the same warning on the scheduled post. The app also warns before an import that would use most of a day's writes with the day's earlier imports. It never stops you, since your account may already be on Workers Paid.
45
45
 
46
46
  The free Workers plan sends four requests a minute. At 14 messages a second, a common starting rate, that's 56 recipients a minute, and 1,000 subscribers take about 18 minutes. At 50 a second or more, it's 200 a minute.
47
47
 
@@ -62,4 +62,6 @@ Each of these guides stands on its own. Pick the ones you want.
62
62
  - [Connect Claude to the API](../guides/01-connect-claude.md), so Claude can draft, proofread, and schedule posts through the same API the editor uses.
63
63
  - [Notifications through Cloudflare's email](../guides/03-notifications.md), so you hear about problems even when your email provider has stopped sending.
64
64
 
65
+ If someone else writes the newsletter, point them to the docs' Publishing section, written for them, starting with [Bring your existing list](../publishing/01-your-list.md).
66
+
65
67
  When a new release comes out, [Upgrade to a new release](../guides/07-upgrade.md) shows how to move to it.
@@ -75,8 +75,12 @@ CF-Access-Client-Secret headers from your environment with every request.
75
75
  Read GET /api/reference first, and use it for every call.
76
76
  ```
77
77
 
78
+ The reference is also served as Markdown, `GET /api/reference.md`, which reads more easily for Claude than the JSON. So are the docs: `GET /api/docs/publishing.md` lists the publisher's pages, `GET /api/docs/self-hosting.md` this guide's, and each page is at `GET /api/docs/<slug>.md`.
79
+
78
80
  Claude reads the reference, and from there can list your posts, write a draft, and schedule it. The reference lists the body types each route takes. Most take JSON, sent with `Content-Type: application/json`. An image upload takes a multipart form, or the image file itself.
79
81
 
82
+ To bring your subscribers over from another service, ask Claude to import your list. Your instance's `/migrate-list` skill reads the format from `GET /api/imports/format` and converts the old service's export to it. It checks Kestrel's plan against its own counts, and asks for your attestation before it imports anyone as a subscriber. [Bring your existing list](../publishing/01-your-list.md) explains what an import does, and the [import format](../publishing/08-import-format.md) describes every column.
83
+
80
84
  ## Check it
81
85
 
82
86
  1. Ask Claude to call `GET /api/whoami`. The app answers with a program's identity, not yours:
@@ -21,7 +21,7 @@ Cloudflare's email sends only from a domain set up with Email Routing. Use your
21
21
 
22
22
  1. Cloudflare adds MX, SPF, and DKIM records for `newsletter.example.com`, and locks them. Wait until they show as added.
23
23
 
24
- Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Avoid `send.example.com` too, since your email provider sends from it.
24
+ Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Don't send notifications from `send.example.com` either, since your email provider sends from it. Email Routing there to forward readers' replies, as [Connect Amazon SES](../get-started/04-ses.md#3-receive-replies-at-your-from-address) sets up, is fine, since it only receives.
25
25
 
26
26
  ## 2. Verify your address
27
27
 
@@ -0,0 +1,86 @@
1
+ # Bring your existing list
2
+
3
+ Almost no one starts a newsletter with an empty list. If your readers are on another service, bring them over before your first post, with the consent they gave there. It's the **Bring your existing list** step on your dashboard, and **Import** starts it. If you're starting fresh, skip this step: new readers join through [Share your subscribe link](09-share-your-subscribe-link.md).
4
+
5
+ This page covers the file Kestrel reads, what an import does and why, and the limits. It also covers taking your whole list out again at any time, in the same format. The dashboard's **Subscribers → Import** walks you through an import, and Claude reads this same page through the API.
6
+
7
+ ## The file Kestrel reads
8
+
9
+ Kestrel reads one documented format, `kestrel-subscribers/1`: a CSV file with a header row and one row per address. A row says what your old service recorded about a person, and Kestrel decides what that means here. Five columns decide what happens to a row:
10
+
11
+ | Column | What it holds |
12
+ | --- | --- |
13
+ | `email` | The address. Kestrel trims it and ignores capitals when it matches. |
14
+ | `status` | `subscribed`, `pending`, `unsubscribed`, or `none` (an address with no consent claim, present only to carry a bounce or a block). |
15
+ | `opt_in` | How consent was collected: `double`, `single`, or `unknown`. |
16
+ | `origin` | Where consent was collected, as a short name such as `sendy` or `mailchimp`. |
17
+ | `subscribed_at` | When they subscribed, with its time zone, such as `2024-03-01T09:30:00Z`. Optional. |
18
+
19
+ The optional columns carry the rest: a bounce or complaint (`suppression`), when someone confirmed or left, and `evidence`, the old service's own consent details, such as a signup IP or a form name. [Import format](08-import-format.md) describes every column and value.
20
+
21
+ The import screen's **Download example file (CSV)** gives you `kestrel-import-example.csv`, a few rows that show each case. Open it in a spreadsheet to see the shape, and save your file as CSV UTF-8.
22
+
23
+ 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 in any other shape is refused, with what's wrong and where. Convert it first, with Claude or by hand.
24
+
25
+ ## A plain list of addresses
26
+
27
+ If all you have is addresses, a `.txt` file with one address per line works too. After you choose it, you say what the addresses are: **Subscribed** (the default), **Unsubscribed**, or **Bounced**. Every address in the file is that one kind, so a plain list of subscribers and a plain list of bounces are two imports.
28
+
29
+ A plain list loses what a full CSV carries: how and where each person joined, when, and the evidence. Kestrel records how they joined as unknown. When your old service can export more than addresses, prefer the CSV.
30
+
31
+ ## Let Claude convert your export
32
+
33
+ Exports from Sendy, Mailchimp, Substack, and other services all differ, and change without notice. If you use Claude Code with your instance, ask it to do the conversion:
34
+
35
+ ```
36
+ Please import my Sendy subscribers from ~/Downloads/sendy-export.csv into the newsletter.
37
+ ```
38
+
39
+ Claude's `/migrate-list` skill reads Kestrel's format from your instance and asks you what anything ambiguous in the export means. It converts a few rows for you to check, then checks Kestrel's plan against its own counts. It asks for your attestation before it imports anyone as a subscriber. [Connect Claude to the API](../guides/01-connect-claude.md) sets Claude up.
40
+
41
+ If a scheduled post would reach the people the import adds, Claude stops and asks you to finish in the dashboard: choose the converted file Claude left in your working folder under **Subscribers → Import**, and the same plan appears for you to confirm.
42
+
43
+ ## What an import does
44
+
45
+ Nothing changes when you choose a file. Kestrel plans the import first and shows you what it would do to every row. You see who is added as a subscriber, who is recorded as having left, and who is suppressed, and which rows are repeats, invalid, or left out. You can download the rows that won't be imported as subscribers, to check them against your file. Only when you press **Import** does anything change, and then the import applies whole, or not at all. If a reader's consent changed between your plan and the import, for example someone unsubscribed or confirmed, their row follows your list as it stands then, and the result lists each row that turned out differently from the plan you saw. An import sends no email.
46
+
47
+ To import anyone as a subscriber, you confirm that everyone you're importing as subscribed opted in to receive your publication, and that the list isn't purchased, rented, or scraped. If you're unsure about some people, take them out of the file before importing. Kestrel keeps your attestation with the import's record.
48
+
49
+ If a post is scheduled, it goes to the list as it stands when it sends, so the people an import adds would receive it. The plan names each such post and how many more people it would reach, and you acknowledge each one before importing.
50
+
51
+ If your list or its scheduled posts changed since the plan in a way the plan rests on, such as another import, Kestrel plans your file again and shows you the new plan as things stand now, before anything happens.
52
+
53
+ ## Why your old service's record is honored
54
+
55
+ Kestrel normally confirms every new subscriber with double opt-in: they click a link in an email before they get any posts. Asking your whole list to confirm again would lose readers who already said yes, and would mail every one of them a request they didn't expect. So an import carries over what your old service recorded instead: the consent each person gave there, how it was collected, and your word for it.
56
+
57
+ Kestrel holds a few lines that an import can't cross:
58
+
59
+ - **Someone who left stays gone.** An address that unsubscribed here is never subscribed again by an import, whatever the file says. Only that reader can come back, by confirming here.
60
+ - **A bounce, a block, or a do-not-contact marker here always wins.** An import never lifts a suppression, so an address known to fail is never mailed. An address you erased keeps only a do-not-contact marker, and an import never records a subscriber for it again, not even as one who left.
61
+ - **A reader's newer choice stands.** If someone confirmed here after your old service says they left, they stay subscribed.
62
+ - **Repeats are folded together,** and when one row says subscribed and another says unsubscribed, unsubscribed wins.
63
+
64
+ In the subscriber list, imported subscribers are **Confirmed** and receive your posts. The **How they joined** column says they were imported, with the service and the opt-in method your file gave, such as "Imported · Sendy, single opt-in". Only someone who confirmed through Kestrel itself reads as "Double opt-in". Choose **View subscriber** from a subscriber's ⋯ menu to see the evidence.
65
+
66
+ ## Limits
67
+
68
+ - **Rows.** One import takes up to 10,000 rows on Workers Paid, or 1,000 on the free plan's limits. The import screen says which applies to you. Split a larger list into several files, and import them one at a time.
69
+ - **Daily database writes on the free plan.** Cloudflare's free plan allows a fixed number of database writes a day, and stops your newsletter until midnight UTC once they're used up. Bringing in a large list, in one file or several, and sending to it on the same day can use them up. So when the day's imports would use most of the day's writes, the plan warns you and suggests importing the file the next day, by UTC, and a post scheduled for a day imports have written warns you too, suggesting it go out the next day. Neither warning stops anything.
70
+ - **When to move to Workers Paid.** From about 5,000 subscribers, it's recommended to move to the $5 Workers Paid plan, which has no daily limit on writes. Whoever runs your Kestrel makes the change, as [Go live](../get-started/06-go-live.md#as-your-list-grows) describes.
71
+
72
+ ## When readers reply
73
+
74
+ Kestrel only sends mail. A reader who replies to a post writes to your From address, so replies reach you once that address forwards to an inbox you read. Whoever runs your Kestrel sets that up outside Kestrel.
75
+
76
+ When a reader replies asking to be taken off the list, carrying it out is up to you, since Kestrel never sees the reply. Find them under **Subscribers**, and choose **Unsubscribe** from their ⋯ menu. It takes effect at once. **Unsubscribe** is there for confirmed readers; one who never confirmed gets no posts, and **Erase** removes them.
77
+
78
+ When a reader asks you to delete their data, choose **Erase** from their ⋯ menu instead. It removes the subscriber and how they joined, and can't be undone. Past send records show an erased recipient in their place. Kestrel keeps only a do-not-contact marker, or the bounce, complaint, or block the address already had, so the address is never mailed again. The reader can still rejoin by subscribing and confirming themselves.
79
+
80
+ ## Taking your list out
81
+
82
+ **Subscribers → Export** downloads your whole list as one CSV in the same format: every subscriber with their state, how they joined and the evidence, and every bounced, complained, blocked, or erased address. It's yours to keep as a backup, or to import into another Kestrel, which carries the list over as it is. A value a spreadsheet would run as a formula is written as text, so the file is safe to open.
83
+
84
+ Claude can read the same export a page at a time through the API, at `GET /api/subscribers/export`.
85
+
86
+ Next, [Write your first post](04-write-a-post.md).
@@ -0,0 +1,35 @@
1
+ # Sending rules
2
+
3
+ Email law asks a few things of every newsletter. Kestrel handles the ones a sending app can, and the rest are yours, since they depend on what you write and whom you write to.
4
+
5
+ This page covers what Kestrel handles, what you handle, and how to act on a reader's request to leave or be deleted. It describes US law (CAN-SPAM) unless it says otherwise. It's general information, not legal advice. When your situation is unclear, ask a lawyer.
6
+
7
+ ## What Kestrel handles
8
+
9
+ - **Consent.** Kestrel mails only people who confirmed they want your newsletter. Someone who subscribes gets an email with a link, and only once they confirm do they receive posts. People you import come with your old service's record of their consent, and your word for it ([Bring your existing list](01-your-list.md#why-your-old-services-record-is-honored)).
10
+ - **Unsubscribing.** Every email has an unsubscribe link, and the one-click unsubscribe header that Gmail and Yahoo ask bulk senders for. It takes effect at once. US law allows up to 10 business days.
11
+ - **Bounces and complaints.** An address that bounces, or a reader who marks your email as spam, is never mailed again. So a bad address can't keep hurting your sending reputation.
12
+ - **Sender authentication.** Your emails are signed with DKIM for your own domain, and SPF and DMARC records in its DNS vouch for them. Whoever runs your Kestrel sets these up, and mailbox providers now expect them.
13
+
14
+ ## What you handle
15
+
16
+ - **An honest From name and subject line.** The From name should say who the email is from, and the subject should match what's inside. Whoever runs your Kestrel sets the From name and address.
17
+ - **Marking ads as ads.** When an email is an advertisement, say so clearly. In a newsletter that's mostly writing, labeling a sponsor's section is enough.
18
+ - **A mailing address, when an email is commercial.** Most newsletters don't need one. [When to provide a mailing address](03-name-your-publication.md#when-to-provide-a-mailing-address) says when you do, beside where you add one.
19
+ - **Requests readers send you.** A reader who replies to a post writes to your From address. Replies reach you once that address forwards to an inbox you read, which whoever runs your Kestrel sets up. Kestrel never sees a reply, so carrying out what a reader asks is up to you.
20
+
21
+ ## When a reader asks to leave or to be deleted
22
+
23
+ Kestrel offers readers no form for deleting their data, so they ask you. In many places the law gives a reader that right. The European Union's GDPR, for example, may apply if you have readers there.
24
+
25
+ - **Say how to reach you.** Your privacy notice, wherever you publish it, should say how readers ask you about their data. Replying to any post works once replies reach your inbox.
26
+ - **To stop the email,** find a confirmed reader under **Subscribers**, and choose **Unsubscribe** from their ⋯ menu. It records that they asked, and they can rejoin by subscribing again. A reader who never confirmed gets no posts, so **Erase** is the choice for them.
27
+ - **To stop mailing someone on your own judgment,** not at their request, choose **Block** instead. It stops every email to the address whatever its status, and **Unblock** undoes it.
28
+ - **To delete their data,** choose **Erase** from the same menu. It removes the subscriber and how they joined, and can't be undone. Past send records show an erased recipient in their place. Kestrel keeps the address only so it's never mailed again, as [Bring your existing list](01-your-list.md#when-readers-reply) explains.
29
+ - **Act promptly.** Under the [GDPR](https://eur-lex.europa.eu/eli/reg/2016/679/oj), you answer within a month in most cases (Article 12). The right to have data deleted is Article 17, and Article 13 asks you to tell readers about it when they sign up.
30
+
31
+ ## Outside the US
32
+
33
+ Other countries have their own rules, and they often ask more. Canada's anti-spam law (CASL) usually counts more email as commercial, and asks for a mailing address in it ([Government of Canada guidance](https://ised-isde.canada.ca/site/canada-anti-spam-legislation/en)). The GDPR covers how you keep readers' data. If many of your readers live outside the US, check the rules where they live.
34
+
35
+ The FTC's [CAN-SPAM compliance guide](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business) is the plain-language source for the US rules on this page.
@@ -0,0 +1,106 @@
1
+ # Name your publication
2
+
3
+ Give your newsletter a name and a look before your first post. Readers see your name, tagline, and logo in every email and on every public page. It's the first step of **Set up your publication** on your dashboard.
4
+
5
+ You set them under **Settings → Publication identity**. Every email takes its look from one template, which works as it comes and which you can edit on the **Template** page. This page also covers the optional mailing address, what a change does to scheduled posts, and who sets the From name and address.
6
+
7
+ ## Name, tagline, and logo
8
+
9
+ All three are optional, and you can change them at any time under **Settings → Publication identity**. The name and tagline save with **Save changes** in the bar at the bottom of the page. The logo saves the moment you pick it.
10
+
11
+ - **Name.** Left blank, your publication uses the From name of your emails instead. If that has no name either, your publication goes unnamed, and every page reads whole without one.
12
+ - **Tagline.** A short line under the name.
13
+ - **Logo.** A PNG, JPEG, WebP, or GIF image. Kestrel shows it square everywhere, so a wider or taller image is cropped to its center square. Pick one at least 256 × 256 px. **Replace** swaps it, and **Remove** takes it away.
14
+
15
+ Sent emails never change. An email already sent, and its archive page, keep the logo and name they went out with.
16
+
17
+ ## Where each part appears
18
+
19
+ Your identity shows in four places:
20
+
21
+ - **In your emails,** wherever your template prints it. The built-in template signs off each post with your logo, name, and tagline. The sample email under **Settings → Email template** shows which parts your template prints.
22
+ - **On the public pages readers see.** Your home page, your archive, and each post's page carry your identity. So do the pages for subscribing, confirming, and unsubscribing. Your logo is the browser tab's icon. **Settings → Reader pages** shows each of these pages as a reader gets it.
23
+ - **In the confirmation email** a new subscriber gets, which opens with your logo, name, and tagline.
24
+ - **In the dashboard.** The top of the sidebar shows your logo, name, and tagline.
25
+
26
+ A post's page shows your publication as it is now at the top, and the sign-off as it was sent.
27
+
28
+ ## When to provide a mailing address
29
+
30
+ **Probably not, if your newsletter is mostly your own writing. Yes, if you use it to sell.** The address is optional, and you can add it or remove it at any time.
31
+
32
+ US law (CAN-SPAM) requires a postal address in commercial email. That's email whose primary purpose is to advertise or promote a product or service, at any volume. In its [2008 rule](https://www.ftc.gov/sites/default/files/documents/federal_register_notices/definitions-and-implementation-under-can-spam-act-16-cfr-part-316/080521canspamact.pdf), the FTC treats a newsletter someone subscribed to as a "transactional or relationship" message instead. That holds when it's mainly content, even with ads mixed in. A subscription that's purely advertising, like a catalog, stays commercial.
33
+
34
+ An email whose primary purpose is selling is a different matter. The FTC's [primary-purpose test](https://www.ecfr.gov/current/title-16/chapter-I/subchapter-C/part-316/section-316.3) looks at what the email says and what its subject line suggests. So you probably need an address in:
35
+
36
+ - An email that's mainly selling, such as a launch, a sale, a paid course, or event tickets.
37
+ - A post that's mostly sponsor content or promotions.
38
+ - A mixed email whose subject line reads like an ad.
39
+
40
+ A nonprofit isn't exempt as a kind of sender. What the email says decides it. Canada's anti-spam law (CASL) usually counts more email as commercial, and asks for a mailing address in it too ([Government of Canada guidance](https://ised-isde.canada.ca/site/canada-anti-spam-legislation/en)). Gmail and Yahoo don't ask for one: their [rules for bulk senders](https://support.google.com/a/answer/14229414) are authentication, one-click unsubscribe, and a low spam-complaint rate.
41
+
42
+ **What counts.** A street address, a USPS P.O. box, or a private mailbox registered with the Postal Service. You never have to publish a home address.
43
+
44
+ **Adding it.** Under **Settings → Publication identity**, choose **Add a mailing address**. The built-in template and both starting templates print it in the footer as soon as it's set, and print nothing while it isn't. They keep it off each post's public page, so your address isn't on the web.
45
+
46
+ If you've added an address and your template doesn't include `{{ .Publication.Address }}`, Kestrel saves your change and warns you. A line under the address says so in Settings, and the **Template** page says so when you save. If you typed your address into the template yourself, or don't need one, you can ignore the warning.
47
+
48
+ This is general information, not legal advice. The FTC's [CAN-SPAM compliance guide](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business) is the plain-language source, and [Sending rules](02-sending-rules.md) covers the rest of what the law asks of a newsletter.
49
+
50
+ ## The email template
51
+
52
+ The template is the layout every post is sent in: HTML with a style block, and variables Kestrel fills in when it sends. There is one template, used by every post. Choose **Template** in the sidebar, or **Edit template →** under **Settings → Email template**.
53
+
54
+ You start from a built-in default. **Start from example** offers two starting templates to replace what's in the editor:
55
+
56
+ - **Signed:** your post, then your logo, name, and tagline as a sign-off.
57
+ - **Plain:** just your post and the footer links.
58
+
59
+ Both put the unsubscribe link, a View in browser link, and your mailing address in the footer, and both end with "Powered by Kestrel". From there, edit the HTML as you like, then press **Save changes**. **Discard** throws your edits away.
60
+
61
+ ### Variables
62
+
63
+ A variable is replaced with its value in each email. Type it exactly as shown, or it renders as empty. The **Variables** list at the bottom of the **Template** page says what each one holds.
64
+
65
+ | Variable | What it holds |
66
+ | --- | --- |
67
+ | `{{ .Post.Body }}` | Your post, rendered from Markdown. Required. |
68
+ | `{{ .Post.Subject }}` | The post's subject line. |
69
+ | `{{ .Publication.Name }}` | Your publication's name. |
70
+ | `{{ .Publication.Tagline }}` | Your tagline. |
71
+ | `{{ .Publication.Logo }}` | Your logo as an image, or nothing when no logo is set. |
72
+ | `{{ .Publication.LogoURL }}` | Your logo's address, or empty. |
73
+ | `{{ .Publication.Address }}` | Your mailing address, or empty. |
74
+ | `{{ .Email.SentTo }}` | The address this email was sent to. |
75
+ | `{{ .Email.UnsubscribeURL }}` | This reader's one-click unsubscribe link. Required. |
76
+ | `{{ .Email.ViewInBrowserURL }}` | The post's public page. |
77
+
78
+ Kestrel won't save a template without `{{ .Post.Body }}` or `{{ .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.
79
+
80
+ ### Email-only parts
81
+
82
+ Wrap part of the template in `{{ if .IsEmail }}` and `{{ end }}` to keep it off each post's public page. The starting templates wrap the unsubscribe link, the View in browser link, and the mailing address this way. To show your address on the web too, move it outside the wrapped part.
83
+
84
+ ### Test the template
85
+
86
+ The sample email at the top of the **Template** page shows the layout with sample content. Mail apps each draw email their own way, so send yourself a test to see it in a real inbox.
87
+
88
+ **Send test email** sends a sample post through your saved template, to the addresses you choose. Your default test recipients, from **Settings → Default test recipients**, fill in first. With unsaved edits, the button reads **Save & send test**, since a test always uses the saved template.
89
+
90
+ ## Changes and scheduled posts
91
+
92
+ When you schedule a post, Kestrel makes its email then, with your template and identity as they stand. So when you change the template, or a part of your identity the template prints, Kestrel makes each scheduled post's email again. Nothing changes until you confirm.
93
+
94
+ When you save, a dialog lists the scheduled posts by subject and send time. Each email's look changes, while its content and send time stay the same. **Save and apply** goes ahead, and **Cancel** leaves your edits unsaved. A logo upload or removal asks the same way.
95
+
96
+ Afterwards, each of those posts says its look changed since your last test. The mark shows in the editor and on the dashboard until you send a fresh test. Send one, since your earlier test no longer shows what goes out. [Scheduling and sending](05-scheduling-and-sending.md) covers tests of a scheduled post.
97
+
98
+ - **Changing a part the template doesn't print** asks nothing, since it doesn't change any email.
99
+ - **A post about to send** blocks the change. Kestrel says when you can try again, once that post has gone out.
100
+ - **Posts already sending or sent** never change.
101
+
102
+ ## Who sets the From name and address
103
+
104
+ Whoever runs your Kestrel sets the From name and address your emails come from. You can't change them in the dashboard, and **Settings → Email sender** shows them read-only. [Connect Amazon SES](../get-started/04-ses.md#8-set-the-provider-and-deploy) covers setting them.
105
+
106
+ Next, [Check your setup](10-check-your-setup.md).
@@ -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 `![cover.jpg](cover.jpg)`. 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: `![A stack of paperbacks on a windowsill](cover.jpg)`.
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).