personal-newsletter

Readme

Last updated: 5/8/2026

Personal Newsletter — How to Use and Write

This folder is where the content of every newsletter issue lives. The CLI tooling lives at ~/Projects/personal-newsletter/ and reads from this folder via a symlink (~/Projects/personal-newsletter/issues → this folder).

Two-folder split

ConcernLives atWhy
Issue drafts (markdown)~/notes/areas/personal-brand/personal-newsletter/ (here)Content. Synced via Syncthing. Searchable in Obsidian. Linkable from /now, dailies, worklogs.
CLI tooling (Python)~/Projects/personal-newsletter/Code. Versioned in its own git repo. Runs newsletter command.
Sent archive (immutable)~/Projects/personal-newsletter/sent/Sent issue manifests with Resend message IDs + recipient snapshots. Stays in project repo for clean separation.
Subscriber state~/notes/areas/relationships/relationship-data/{slug}.md frontmatterPer-person fields: email, newsletter_subscribed, newsletter_frequency, newsletter_confirmed_at, newsletter_unsubscribed_at, newsletter_source.

Writing a new issue

  1. Create a new file in this folder named YYYY-MM-DD-issue-N.md (or whatever's meaningful).
  2. Use frontmatter for the email subject:
    yaml
    ---
    subject: "What I'm up to (a note from Matt)"
    ---
  3. Write your content as plain markdown. Standard markdown features supported:
    • Headings (#, ##, ###)
    • Lists (bulleted, numbered)
    • Inline links ([text](url))
    • Bold (**word**), italic (_word_), inline code (`code`)
    • Block quotes (> ...)
    • Horizontal rules (---)
    • Tables, strikethrough
  4. Use {{first_name}} anywhere in the body — it's replaced per-recipient at send time.
  5. Keep it short (200–400 words). Five short sections beats one long one.
  6. The first H1 (# heading) is treated as the in-body title and stripped from email body if it equals the subject.

Voice guidelines (per resources/matts-writing-style-profile.md)

  • Direct, warm, unpolished. Short paragraphs. Contractions always.
  • No em-dashes. No "delve", "leverage", "synergy", "I'm excited to share", "I hope this email finds you well."
  • "I think", "I believe", "My conjecture is..." for genuine opinions. State emotions plainly.
  • Closing: Best,\n-Matt
  • Greeting: Hey {{first_name}},

Recommended section structure

Per the sustainability research — 1+ year personal-newsletter operators converge on fixed headers:

  1. What I'm working on — current focus, projects, work
  2. What I'm building / reading / curious about — content + interests
  3. Where I am — physical location, travel
  4. What I'm thinking about — open questions, asks
  5. How can I help? — invite reciprocity (Matt's signature CTA)

You don't have to use these exact headers, but the principle is: fill in known sections, don't compose from a blank page.

Sending an issue (CLI workflow)

All commands run from ~/Projects/personal-newsletter/. The venv is at .venv/.

0. First-time / sanity check

bash
cd ~/Projects/personal-newsletter
.venv/bin/python -m newsletter doctor

Should show all green. Verifies env vars (RESEND_API_KEY, RESEND_FROM, etc.), Python deps, vault path, Resend domain.

1. Refresh recipient candidates from your relationship-data

bash
.venv/bin/python -m newsletter scan-relationships --strength 1-5 --min-name-match 2

Scans ~/notes/areas/relationships/relationship-data/*/ for emails. Output: recipients-candidates.csv at the project root. Includes a column n_emails_for_slug flagging people with multiple candidate addresses (you pick one when curating).

To find people you know but for whom no email is captured (for the contact-asker workflow):

bash
.venv/bin/python -m newsletter find-no-email --limit 30

Output: no-email-candidates.csv sorted by signal strength (more files / messages / deep-research = stronger relationship).

2. Curate the recipient list

Open recipients-candidates.csv and:

  • Set include to y on rows you want to send to.
  • For people with multiple candidate emails (n_emails_for_slug > 1), mark exactly ONE.
  • Save as recipients-active.csv.

Validate:

bash
.venv/bin/python -m newsletter validate-recipients recipients-active.csv

Errors on: invalid email syntax, duplicate emails, multiple emails marked for the same person.

3. Render preview

bash
.venv/bin/python -m newsletter render --plaintext --check issues/YYYY-MM-DD-issue-N.md

Writes ~/Projects/personal-newsletter/build/{name}.html and .txt. Open the HTML in a browser to spot-check.

4. Test-send to yourself

bash
.venv/bin/python -m newsletter test-send issues/YYYY-MM-DD-issue-N.md

Sends a single message to MATT_TEST_EMAIL (from .env, defaults to matt@matthandzel.com). No recipient list involved. Subject can be overridden with --subject "[TEST] ...".

5. Run mail-tester.com (recommended for first issue or after layout changes)

Get a fresh test address from https://www.mail-tester.com/, then:

bash
MATT_TEST_EMAIL=test-XXXX@srv1.mail-tester.com .venv/bin/python -m newsletter test-send issues/YYYY-MM-DD-issue-N.md

Visit https://www.mail-tester.com/test-XXXX to see the score. Aim for ≥9/10.

6. Dry-run the real send

bash
.venv/bin/python -m newsletter send issues/YYYY-MM-DD-issue-N.md --recipients recipients-active.csv --dry-run

Prints a table of recipients + the subject. No API calls.

7. Real send

bash
.venv/bin/python -m newsletter send issues/YYYY-MM-DD-issue-N.md --recipients recipients-active.csv

Sends individual personalized messages via Resend's REST API. Each message has:

  • From: Matt Handzel <newsletter@matthandzel.com>
  • Reply-To: newsletter@matthandzel.com (forwards to your main inbox)
  • List-Unsubscribe: <mailto:newsletter+unsubscribe@matthandzel.com>
  • List-Unsubscribe-Post: List-Unsubscribe=One-Click
  • Multipart: HTML + plain-text alternative
  • First-name greeting personalized per recipient

After: a manifest is archived to ~/Projects/personal-newsletter/sent/{slug}-{ISO8601}/ with manifest.json (Resend message IDs, recipients), body.html, body.txt, source.md.

To stage a warm-up ramp (issue 1 ~25 → issue 2 ~60 → issue 3 ~150), use --limit N.

Cadence (planned for v1)

Two frequencies will be supported once the website preferences flow ships:

  • quarterly — default. Issues sent end of March / June / September / December.
  • yearly — only the December issue (year in review).

newsletter send --cadence quarterly will only include subscribers whose newsletter_frequency is quarterly or unset. --cadence yearly is the December annual issue and goes to everyone (quarterly + yearly + unset).

Until that ships, every send goes to every confirmed recipient regardless of preference.

File layout (cheat sheet)

~/notes/areas/personal-brand/personal-newsletter/   <-- you are here
  README.md                                         <-- this file
  YYYY-MM-DD-issue-N.md                             <-- issue drafts
  _smoke-test.md                                    <-- throwaway test draft

~/Projects/personal-newsletter/                     <-- CLI lives here
  .env                                              <-- secrets (gitignored)
  pyproject.toml                                    <-- Python package
  src/newsletter/                                   <-- source
  tests/                                            <-- pytest suite
  issues/                                           <-- SYMLINK to vault folder
  sent/{date-slug}/                                 <-- archived sends
  build/                                            <-- render output (gitignored)
  recipients-candidates.csv                         <-- output of scan-relationships
  recipients-active.csv                             <-- you curate this
  no-email-candidates.csv                           <-- output of find-no-email
  PLAN.md                                           <-- task decomposition
  ENGINE-LOG.md                                     <-- audit trail
  STATE.md                                          <-- handoff state dump

~/notes/projects/personal-newsletter/               <-- vault project doc
  personal-newsletter.md                            <-- description
  _worklog.md                                       <-- project worklog
  SPEC.md                                           <-- full spec
  research/                                         <-- research files

Anti-goals (per SPEC.md)

  • No marketing CSS / SaaS-launch templates. Personal email aesthetic.
  • No URL shorteners (bit.ly, t.co).
  • No mass greetings ("Hey there!", "Friend!"). First names only.
  • No apologies for not-having-sent-earlier in a follow-up issue. Per sustainability research, this is the dominant abandonment trigger — just send.
  • Skip a quarter without explanation if life happens. The next issue does not catch up, justify, or apologize. It just sends.