@kurtbruns/kestrel 1.10.0 → 1.12.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 +64 -1
- package/README.md +17 -28
- package/cli/reset.mjs +2 -0
- package/cli/seed.mjs +2 -0
- package/demo/README.md +4 -2
- package/demo/posts/0-test-email/index.md +13 -0
- package/demo/posts/5-a-quick-note/index.md +2 -0
- package/dist/public/dashboard/app.js +302 -293
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +434 -54
- package/dist/worker/index.js +1472 -470
- package/docs/README.md +2 -2
- package/docs/get-started/02-deploy.md +12 -5
- package/docs/get-started/03-access.md +2 -0
- package/docs/get-started/04-ses.md +36 -16
- package/docs/get-started/05-verify.md +36 -8
- package/docs/get-started/06-go-live.md +13 -55
- package/docs/guides/01-connect-claude.md +7 -5
- package/docs/guides/03-notifications.md +10 -10
- package/docs/guides/09-as-your-list-grows.md +62 -0
- package/docs/publishing/01-your-list.md +118 -47
- package/docs/publishing/02-sending-rules.md +5 -5
- package/docs/publishing/03-name-your-publication.md +7 -93
- package/docs/publishing/04-write-a-post.md +37 -66
- package/docs/publishing/06-troubleshooting.md +103 -0
- package/docs/publishing/07-working-with-claude.md +19 -37
- package/docs/publishing/09-share-your-subscribe-link.md +11 -59
- package/docs/publishing/10-check-your-setup.md +12 -30
- package/docs/publishing/11-email-template.md +34 -0
- package/docs/publishing/README.md +12 -11
- package/docs/reference/01-configuration.md +6 -6
- package/docs/reference/02-sending-domain-dns.md +1 -1
- package/migrations/0007_sends_progress_at.sql +12 -0
- package/migrations/0008_notifications_sandbox.sql +35 -0
- package/migrations/0009_post_email_only.sql +12 -0
- package/migrations/0010_unlisted_links.sql +43 -0
- package/migrations/0011_sends_render_warnings.sql +13 -0
- package/package.json +1 -1
- package/template/claude/CLAUDE.md +5 -2
- package/template/claude/skills/migrate-list/SKILL.md +2 -2
- package/docs/guides/08-try-locally.md +0 -108
- package/docs/publishing/05-scheduling-and-sending.md +0 -72
- package/docs/publishing/06-after-a-send.md +0 -109
- package/docs/publishing/08-import-format.md +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,67 @@ To move a running instance from one version to another, follow [Upgrade to a new
|
|
|
10
10
|
|
|
11
11
|
<!-- Add entries under Added / Changed / Fixed / Breaking. One operator-facing line each; see .claude/rules/changelog.md. Upgrade steps (a migration, a new setting) go once, in the Upgrading paragraph, never on an entry. -->
|
|
12
12
|
|
|
13
|
+
## [1.12.0] - 2026-10-16
|
|
14
|
+
|
|
15
|
+
**Upgrading from 1.11.0.** Five database migrations, which `npm run sync-migrations` brings in: apply them before you deploy with `npx wrangler d1 migrations apply DB --remote --env production` (and `--local` for local development). `0007_sends_progress_at` adds a column recording when each send last handed anyone off, and writes no rows. `0008_notifications_sandbox` rebuilds the small notifications table to admit a new kind, copying its rows once. `0009_post_email_only` adds each post's listing, reading every existing post as listed in the archive, so the archive shows what it did before; it adds a column with a default and rewrites no rows. `0010_unlisted_links` gives every post its unlisted link, and records that every send made before it links its archive address, so those addresses keep answering whatever the post's listing; a send scheduled between the migration and the deploy counts as one made before. It writes each post row once. `0011_sends_render_warnings` adds a column keeping what the render found wrong with each scheduled email, reading every earlier send as having found nothing; it adds a column with a default and rewrites no rows. Run `npm run check-context` in your instance's repository: this release changes the operator's Claude context, and a copy you've edited is merged by hand. An instance verified with an earlier release has its test post in the archive: open it under **Sent** and select **Remove from archive…**. Two API changes can break a script of yours, listed under Breaking: one that reads a send's halt `cause` reads new values, and one that sets a slug by hand handles a 409 `slug_taken` by choosing another, where it used to get a numbered slug.
|
|
16
|
+
|
|
17
|
+
### Breaking
|
|
18
|
+
|
|
19
|
+
- A send's halt `cause`, on the API and in the `refused` condition, takes new values so each names its own fix: `opt_in`, `sending_pool`, `unsupported`, `mail_from`, `sandbox`, `paused`, `set_paused`, and this send's own `content` and `invalid_request`. `suspended` now means only an account AWS shut down (a paused account or configuration set is `paused` or `set_paused`), and an SES MAIL FROM refusal is `mail_from` rather than `sender`.
|
|
20
|
+
- A slug sent by hand (`slug` on `POST /api/posts`, `PUT /api/posts/:id`, `PUT /api/posts/:id/settings`, or a schedule) that another post holds is now refused with a 409 `slug_taken`, and one that isn't lowercase words joined by single hyphens with a 400 `slug_invalid`, where before it was numbered or rewritten silently. A change to a sent post's slug is a 409 `slug_permanent` (SPEC §4).
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `GET /api/whoami` names a service token by its Client ID, as `tokenId`, so you can check which token Claude is using, and Resolve's note on a wedged send names the token that resolved it. Each service call is logged as `auth.service`, and an Access sign-in that is neither a person nor a service token as `auth.unidentified` (warn), with the names of the claims it carried and never their values (SPEC §11, §12).
|
|
25
|
+
- The publisher is emailed, at most once a day and naming no one, when the SES sandbox refuses a signup's confirmation, since the reader was told a link is on its way (SPEC §7, §8).
|
|
26
|
+
- A recent send with no delivery, bounce, or complaint receipt at all an hour after it went out says so, amber, on its record and the dashboard, as `no_receipts` in its conditions: SES's reports are probably not reaching the webhook, so its bounces and complaints suppress no one. Connect Amazon SES now sets a longer SNS retry policy on the topic, and mentions a dead-letter queue as optional (SPEC §8).
|
|
27
|
+
- **Email only**: a post can be sent to subscribers without being listed in the public archive or on the home page, and its archive address answers nothing (SPEC §5). Uncheck **Include in archive** in the editor's new **Post settings**, or in the Schedule, Send now, or Edit schedule dialog, and change it later on a sent post's page (**Remove from archive…**, which takes its archive address down while keeping it the post's, or **Add to archive…**; both ask first). Its page answers at the email's unlisted link and asks search engines not to index it. A draft's head and the Drafts and Sent lists mark it with an envelope and "Email only" in muted text, a scheduled post's banner says where it's going, and a post in the archive carries no mark. The API carries it as `email_only` on posts and sends, on the schedule and Send now bodies, and through `PUT /api/posts/:id/settings`, which also sets the slug; a reschedule still moves only the time. Every post the API answers carries its two addresses, absolute, as `archive_url` and `unlisted_url`, with `published_at` (when its send started) and `archive_live` (whether the archive address shows it now).
|
|
28
|
+
- **Unlisted links**: every email's "view in browser" link is now the post's own permanent link, `/email/<token>` on the app's origin, in place of its archive address (SPEC §5). It forwards to the archive page while the post is listed and is the page while it's email only, so changing `ARCHIVE_BASE_PATH` or moving the archive no longer breaks links in emails sent from now on. Emails sent before keep linking their archive address, which keeps working. The API carries `unlisted_token` on a post and `links.unlisted` on a send.
|
|
29
|
+
- The operator's Claude context tells Claude to say whether a post will be in the archive, and at which address, before scheduling or sending it; to send it as email only only when the publisher asks; and to confirm before taking a sent post out of the archive or putting it in, which the API doesn't ask about the way the dashboard does. `npm run check-context` offers the change to an existing instance.
|
|
30
|
+
- The dashboard keeps setup's steps after the first post, as a **Finish setting up** card, until the first post listed in the archive goes out. Its last step, **Publish your first post** (formerly Write your first post), is the one that clears it, done once a post that goes to subscribers and onto the site starts sending, so a test send that's email only, like Verify it works' own, leaves the publisher their guided start. **Check your setup** is guidance and never shows as done.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The operator's Claude context: on `remake_required`, Claude now tells the publisher which scheduled posts a template or identity change would re-make and when each fires, and confirms only on their word, then tests each re-made send. Re-making a frozen email is the publisher's deliberate act (SPEC §2), which the editor asks about and the API leaves to the client.
|
|
35
|
+
- A post save refused as 409 `stale_revision` now carries the post as it stands and its `markdown`, so a client can re-apply its change without reading the post again.
|
|
36
|
+
- A route that takes no body (a post's preview, cancelling a send, and the like) now accepts a request declaring `Content-Type: application/json` with an empty body, which HTTP helpers send by habit, instead of refusing it with 415. A body sent to such a route is still refused.
|
|
37
|
+
- Each SES refusal gets its own advice, checked against AWS's docs, and says who makes the fix: a paused configuration set, a paused or suspended account, a custom MAIL FROM domain's MX record, a missing dedicated IP pool, an account not signed up for SES, and the sandbox are told apart, and a quota spent in the sandbox points to production access. The line now says "No one has been marked unsent because of this", which stays true on a send with readers unsent for their own reasons. Troubleshooting lists each refusal with its fix (SPEC §12).
|
|
38
|
+
- A rate limit that refuses the first request of three retries in a row shows in amber, on the dashboard and the send's page, that `SES_MAX_SEND_RATE` is set above your SES account's rate, rather than reading as an outage with nothing to do (SPEC §12).
|
|
39
|
+
- The operator's Claude context: the sandbox caution says a send holds in the sandbox, mailing only verified addresses, rather than marking the rest unsent.
|
|
40
|
+
- A send is flagged as **in flight too long** only when it has handed off no one for 30 minutes and nothing else explains it (a provider refusing the account, an outage it is waiting out, or a send awaiting Resolve), rather than 30 minutes after it started. A large send going out at your SES rate on Workers Free is no longer flagged, emailed about, or logged as `send.stuck`, however long it takes; one the sweep has stopped moving still is, shows no time to finish, and its email and message say nothing has gone out and why that points at the sweep (SPEC §12).
|
|
41
|
+
- A post's archive address moves from beside the subject into **Post settings** (top right of the editor), which save nothing until **Save**: one field with the base fixed and only the slug to type, following the subject until you do. A slug that's malformed or another post's is now refused with a message rather than numbered behind your back, and a subject whose slug another post has takes the next number (`spring-update-2`) and says so. The Schedule, Send now, and Edit schedule dialogs show the same settings under the time and apply them with the act, and a scheduled post's banner names where it's going ("Sends by email and publishes at <its archive address>" or "Sends by email only"). The web version's strip now says only that the page isn't public yet. The address is fixed once the post starts sending (SPEC §4). The API refuses a bad slug as `slug_invalid` or `slug_taken`, a change to a sent post's as `slug_permanent`, and answers `subject_slug_held` on a post's read, saves, and settings changes when the subject's address is another post's and the post got a number.
|
|
42
|
+
- A post's archive page is now sent with `Cache-Control: no-cache` (it was cached for an hour), so taking a post out of the archive takes effect at once. Behind a CDN, more of those requests reach the Worker.
|
|
43
|
+
- A sent post's page is split into **The post** and **Delivery** (the record, unchanged). The post shows its link as one field with **Copy** and **Open ↗**, and below it whether the post is in your archive or email only, with **Remove from archive…** or **Add to archive…**.
|
|
44
|
+
- A scheduled send's **Reschedule** is now **Edit schedule**, on its card under Sent and on the editor's banner: one dialog for the send time and the post's settings, with **Save**, which saves either or both, the settings first, and a toast that says whether the send was rescheduled or only its settings saved. The settings alone can be saved up to the send time.
|
|
45
|
+
- A sent post's recipient list opens on **All** rather than **Failures**, its switch now reading **All | Delivered | Failures**. The failures are still counted in the tiles above it, one switch away.
|
|
46
|
+
- Verify it works sends its test post as **Email only**, and ends by erasing the SES simulator addresses and any test subscriber that won't be a reader.
|
|
47
|
+
- The demo publication (`npm run seed`) opens with the operator's own test send, made as Verify it works makes one: email only, to one test address, before the list came in. It shows under **Sent** with the **Email only** label, and the archive leaves it out. Its scheduled post, **A quick note**, says how to send a post as email only.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- A scheduled post keeps its render warnings, such as an image the post references but never uploaded: scheduling and Send now answer with them, and a preview or test of the scheduled email reports them, where before a test of it always came back clean. A template or identity change that re-makes the email keeps the new render's (SPEC §5).
|
|
52
|
+
- An SES refusal of the key's signature that AWS answers with a 400 (an incomplete or invalid signature, an expired request, a missing token) now holds the send as a credentials refusal with no reader consumed, as a wrong key already did, instead of marking each reader failed for good (SPEC §12).
|
|
53
|
+
- In the SES sandbox, a list send no longer marks every unverified reader unsent and finishes quietly. It mails your verified addresses, holds with "The provider is refusing the account" and advice to request production access, and goes to everyone else once AWS grants it, mailing no one twice. A reader whose address contains your From address is no longer taken for an unverified sender, which held every send (SPEC §12).
|
|
54
|
+
- An SES answer that isn't a reader's fault no longer marks a batch of readers unsent. A request SES didn't receive whole or a limit is retried, a refusal of the email itself holds that send for you with SES's words without holding other posts, an invalid batch that names one reader refuses only that reader, the same failure on several readers of one request with none taken holds the batch, and an answer Kestrel doesn't recognize waits for **Resolve…** rather than being guessed. A malformed address on an imported list is left out of the request and marked unsent alone (SPEC §10, §12).
|
|
55
|
+
- A batch SES answers with a server error, or aborts, is no longer sent again, since AWS says such an answer can follow emails it already took: its readers wait for **Resolve…** (SPEC §10).
|
|
56
|
+
- When SES refuses a badly signed request and echoes back what it was sent, the access key ID, the signature, and any session token are now removed from its words before they reach a held send's page, the email about it, a failed delivery's record, or the log (SPEC §9, §12).
|
|
57
|
+
- SES's bounces and complaints are no longer lost when the webhook can't fetch SNS's signing certificate: it answers 503, so SNS retries, where before it refused the message as forged and SNS discarded it for good. A message whose SNS signature doesn't hold, or from another SNS topic, is still refused, now logged as `webhook.refused` at error with the topic that came and the one expected; a request with no SNS signature at all is refused without one (SPEC §10, §12).
|
|
58
|
+
- A test send that isn't sent says why, in the editor and on the API (`not_sent`): your provider's own words and the fix where it's known, so a test the SES sandbox refuses for an unverified address no longer just says to send again. A test your provider never answers says it may have arrived, rather than reading as a server error, and both are logged as `test.refused` and `test.unknown` (SPEC §5, §12).
|
|
59
|
+
- A send that SES keeps partly refusing, as a daily quota frees up a few at a time, is one refusal until SES takes a batch with nothing refused: you're emailed about it once, its page keeps the time the refusal began, and its retries keep spacing out up to an hour apart instead of starting over every few minutes. The readers SES took are still recorded as sent at once, and the rest wait with no retry spent (SPEC §12).
|
|
60
|
+
- A post that comes due while another send is held by SES refusing the account, or by an outage, now waits with it instead of firing: it asks SES nothing, reads as held with the same next retry, and goes out once SES answers. You're emailed once per refusal rather than once per send, and an outage where SES doesn't answer no longer leaves a batch of each new post waiting for Resolve. A held post goes to your list as it stands when it actually goes out (SPEC §6, §12).
|
|
61
|
+
- A send paused for a provider outage reads **Backing off**, with when it next retries, on its page, the Sent list, and the dashboard, and shows no time to finish, even while a batch the provider never answered waits for Resolve. Before, it read as sending normally for up to an hour (SPEC §12).
|
|
62
|
+
|
|
63
|
+
## [1.11.0] - 2026-10-15
|
|
64
|
+
|
|
65
|
+
**Upgrading from 1.10.0.** No database migrations. Run `npm run check-context` in your instance's repository: `/migrate-list` now reads the import format from "Bring your existing list".
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- The Publishing docs are reorganized around what a publisher has to do or decide. Their sections are **Set up your publication**, **Go further** (Edit your email template, Working with Claude), and **Rules and troubleshooting** (Sending rules, Troubleshooting). Write and send a post covers scheduling too, Troubleshooting replaces After a send with each problem a send or an import reports, in its own words, and the import format is the last section of Bring your existing list, which opens with when to upgrade to Workers Paid.
|
|
70
|
+
- In Self-hosting, **As your list grows** is its own guide (raising Amazon SES limits and upgrading to Workers Paid, with how long a send takes after each), Verify it works ends by checking SES production access, and Go live hands over to the publisher, including letting them sign in through Access. The pages call whoever writes the newsletter the publisher. Try Kestrel locally first moves into the README, now the front page for someone deciding whether to run Kestrel, and working on Kestrel's own source moves to CONTRIBUTING.md.
|
|
71
|
+
- The Markdown of each removed page (`import-format`, `scheduling-and-sending`, `after-a-send`, `try-locally`) still answers at its old address, with the page that now holds its content. The import screen's **Import format reference →** opens the file format section.
|
|
72
|
+
- The operator's Claude context: `/migrate-list` reads the import format from `GET /api/docs/your-list.md`, since the separate import format page is gone.
|
|
73
|
+
|
|
13
74
|
## [1.10.0] - 2026-10-14
|
|
14
75
|
|
|
15
76
|
**Upgrading from 1.9.0.** No database migrations. Run `npm run check-context` in your instance's repository: this release changes the operator's Claude context (a new `/migrate-list` skill, and rules for erasing, for choosing between unsubscribing and blocking, and for reading the docs as Markdown), and a copy you've edited is merged by hand. Three API changes can break a script of yours, listed under Breaking. One that adds suppressions sends `reason: "manual"` or none, reads what was stored from the answer's `suppression`, and takes a 200 with `added: false` as already suppressed. One that clears suppressions takes a 409 `erasure_marker` on an erased address as final. One that reads `GET /api/docs` reads `sets` in place of `landing`.
|
|
@@ -422,7 +483,9 @@ The first tagged release: a self-contained newsletter app on a Cloudflare Worker
|
|
|
422
483
|
- A build-version stamp: every instance reports its version, commit, and build time, read-only in the editor and at `GET /api/version`.
|
|
423
484
|
- This changelog and a semantic-versioning release process.
|
|
424
485
|
|
|
425
|
-
[Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.
|
|
486
|
+
[Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.12.0...HEAD
|
|
487
|
+
[1.12.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.12.0
|
|
488
|
+
[1.11.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.11.0
|
|
426
489
|
[1.10.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.10.0
|
|
427
490
|
[1.9.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.9.0
|
|
428
491
|
[1.8.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.8.0
|
package/README.md
CHANGED
|
@@ -1,59 +1,48 @@
|
|
|
1
1
|
# Kestrel
|
|
2
2
|
|
|
3
|
-
Kestrel is
|
|
3
|
+
Kestrel is an open-source newsletter app you run yourself, on Cloudflare and Amazon SES. You own the list, the proof each reader opted in, the record of every send, and a permanent archive of every post.
|
|
4
|
+
|
|
5
|
+
You write in Markdown, preview the exact email, and schedule it with a window to cancel. Bring your list from another service, and take it out whenever you like. Work in the editor, or ask Claude to do it through the same API.
|
|
4
6
|
|
|
5
7
|
See [getkestrel.dev](https://getkestrel.dev) to learn more about Kestrel.
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## Try it locally
|
|
10
|
+
|
|
11
|
+
You can run Kestrel on your own computer to look around, with [Node.js](https://nodejs.org/) 22 or later. It needs no Cloudflare account, and email goes to a stand-in, so nothing reaches a real inbox.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
In an empty folder, write a new instance and install it:
|
|
10
14
|
|
|
11
15
|
```bash
|
|
12
16
|
npx @kurtbruns/kestrel init
|
|
17
|
+
npm install
|
|
13
18
|
```
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
The docs are two sets, one per job. [Self-hosting](docs/README.md) is for whoever deploys and runs Kestrel. [Publishing](docs/publishing/README.md) is for whoever writes and sends the newsletter: setting up the publication, the list, writing, sending, and the rules a newsletter keeps. Both are in the editor's **Docs** tab, which opens on Publishing, and an instance serves every page as Markdown for Claude to read.
|
|
18
|
-
|
|
19
|
-
## Work on Kestrel
|
|
20
|
-
|
|
21
|
-
This repository is Kestrel's source. To run it on your computer, with [Node.js](https://nodejs.org/) 22 or later:
|
|
20
|
+
Copy the local settings, and start the app:
|
|
22
21
|
|
|
23
22
|
```bash
|
|
24
|
-
npm install
|
|
25
23
|
cp .dev.vars.example .dev.vars
|
|
26
24
|
npm run dev
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
The first time, it asks you to confirm the local database's migrations. Then open http://localhost:8787/dashboard/, with nothing to sign in with. If port 8787 is taken, the `Ready on` line shows the one it picked.
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
The app starts empty. To explore it with content, load **Field Notes**, a demo publication kept as Markdown in [`demo/`](demo/):
|
|
29
|
+
In a second terminal, load a demo publication with posts, subscribers, and past sends:
|
|
34
30
|
|
|
35
31
|
```bash
|
|
36
|
-
npm run seed
|
|
37
|
-
npm run simulate-send -- --in 90s # schedule a send and print a link to watch it go out
|
|
38
|
-
npm run reset # back to an empty instance
|
|
32
|
+
npm run seed
|
|
39
33
|
```
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
The editor is one client of Kestrel's HTTP API, and Claude is the other. Locally, mint a token to call it yourself:
|
|
35
|
+
To start over, reset the app to the state a new instance starts in: no posts, subscribers, or settings, with the first-run dashboard and its setup checklist:
|
|
44
36
|
|
|
45
37
|
```bash
|
|
46
|
-
|
|
47
|
-
curl -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/posts
|
|
38
|
+
npm run reset
|
|
48
39
|
```
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
When you're ready to deploy your own instance, see the [setup guide](https://getkestrel.dev/docs/), which contains instructions for deploying and running Kestrel.
|
|
51
42
|
|
|
52
|
-
##
|
|
43
|
+
## Contributing
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
- **Working on the code:** [`.claude/CLAUDE.md`](.claude/CLAUDE.md) has the commands, conventions, and module boundaries.
|
|
56
|
-
- **Releases:** [`CHANGELOG.md`](CHANGELOG.md).
|
|
45
|
+
To work on Kestrel's own source, see [CONTRIBUTING.md](CONTRIBUTING.md). Releases are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
57
46
|
|
|
58
47
|
## License
|
|
59
48
|
|
package/cli/reset.mjs
CHANGED
|
@@ -20,4 +20,6 @@ export async function resetCommand(argv) {
|
|
|
20
20
|
"[reset] done — the local database is a fresh install (no posts, subscribers, or settings).",
|
|
21
21
|
);
|
|
22
22
|
console.log(` view the first-run dashboard: ${url || `${base}/dashboard/`}`);
|
|
23
|
+
// An open editor tab holds the settings it read when it loaded (the old name and logo).
|
|
24
|
+
console.log(" reload any dashboard tab already open, so it reads the fresh install.");
|
|
23
25
|
}
|
package/cli/seed.mjs
CHANGED
|
@@ -142,4 +142,6 @@ export async function seedCommand(argv) {
|
|
|
142
142
|
for (const a of summary.urls.archive) {
|
|
143
143
|
console.log(` archived post: ${a}`);
|
|
144
144
|
}
|
|
145
|
+
// An open editor tab holds the settings it read when it loaded (the old name and logo).
|
|
146
|
+
console.log(" reload any dashboard tab already open, so it reads the new publication.");
|
|
145
147
|
}
|
package/demo/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Everything `npm run seed` loads into a local dev server, apart from the subscrib
|
|
|
6
6
|
- **`posts/`**: one folder per post, a page bundle: the post is `index.md`, and any image it shows sits beside it (`1-the-hovering-hunter/kestrel.webp`). The post refers to the image by its filename, as in ``, so the link resolves when you preview the Markdown, and the seed attaches the file to that post. The number in a folder's name sets its place: sent posts go out oldest first in that order, and a post's id (and so its editor URL) follows its position.
|
|
7
7
|
- **`field-notes-logo.png`**: the publication logo, named by `publication.md`.
|
|
8
8
|
|
|
9
|
-
The worker bundles the Markdown files (the `rules` entry in `wrangler.jsonc`) and parses them in `src/dev/demo.ts`, which lists each post's folder; add a post by adding its folder and one line there. `npm run dev` watches this folder, so an edit here is in the next seed. `cli/seed.mjs` uploads the images through the running dev server, which stores them in local R2. The subscribers, the four completed sends, and the scheduled send's fire time come from the timeline in `src/dev/seed.ts`.
|
|
9
|
+
The worker bundles the Markdown files (the `rules` entry in `wrangler.jsonc`) and parses them in `src/dev/demo.ts`, which lists each post's folder; add a post by adding its folder and one line there. `npm run dev` watches this folder, so an edit here is in the next seed. `cli/seed.mjs` uploads the images through the running dev server, which stores them in local R2. The subscribers, the four completed sends, and the scheduled send's fire time come from the timeline in `src/dev/seed.ts`. The first post, `0-test-email`, is the operator's test send, made the way Verify it works has them make one: email only, to their own second address alone, before the list came in. That address subscribed just before it and unsubscribed just after, so it's in no later send.
|
|
10
10
|
|
|
11
11
|
## Front matter
|
|
12
12
|
|
|
@@ -28,8 +28,10 @@ A post:
|
|
|
28
28
|
|---|---|---|
|
|
29
29
|
| `subject` | yes | The subject line, which is also the post's title in the list. |
|
|
30
30
|
| `slug` | yes | The post's path in the archive. |
|
|
31
|
-
| `status` | yes | `sent`, `scheduled`, or `draft`. The seed timeline has four completed sends, so at most four posts can be `sent`. |
|
|
31
|
+
| `status` | yes | `sent`, `scheduled`, or `draft`. The seed timeline has four completed sends, so at most four posts can be `sent` without `sent_to`. |
|
|
32
32
|
| `edited` | no | Drafts only: when the draft was last edited, as `2 days ago`. |
|
|
33
|
+
| `email_only` | no | Sent or scheduled posts: `true` to send it without listing it in the archive or on the home page. |
|
|
34
|
+
| `sent_to` | no | Sent posts only: a test send to just these addresses, comma-separated, before the list is imported, instead of a timeline send. Keep them on `.example`. |
|
|
33
35
|
|
|
34
36
|
## The images
|
|
35
37
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
subject: Test email
|
|
3
|
+
slug: test-email
|
|
4
|
+
status: sent
|
|
5
|
+
email_only: true
|
|
6
|
+
sent_to: proof@fieldnotes.example
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Test email
|
|
10
|
+
|
|
11
|
+
This is the test send that proved Field Notes works end to end, before its list came in: subscribing, confirming, scheduling, and a real send landing in a real inbox.
|
|
12
|
+
|
|
13
|
+
It went out as email only, so it isn't in the archive or on the home page. Its web version still opens from the email, which is how you're reading this.
|
|
@@ -8,4 +8,6 @@ status: scheduled
|
|
|
8
8
|
|
|
9
9
|
Not every issue has to be long. Sometimes the best newsletter is a quick note that there's something new to read, watch, or listen to.
|
|
10
10
|
|
|
11
|
+
A note like this one doesn't always belong in your archive. To send it as email only, uncheck **Include in archive** in its Post settings.
|
|
12
|
+
|
|
11
13
|
Thanks for reading Field Notes.
|