diff --git a/README.md b/README.md index 5422f03..a1a4f3a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,18 @@ plaintext alternative itself. - **`type`** — defaults to `regular`. `optin` isn't supported and is rejected at sync time. - **`scheduled_at` doesn't exist** — deliberately, since listmonk auto-sends a `scheduled` campaign the moment it fires, which would let a plain `git push` cause a real send. +## Personalization + +Listmonk renders each recipient's campaign body as a Go template before converting Markdown to HTML, so a subscriber's `attribs` can be referenced directly in `campaign.md`'s body. `eec`'s signup forms set `first_name` (and `last_name`, if given) as attribs on every subscriber who provides one — subscribers who signed up before that (or via the public form when it collected email only) won't have it set. + +Verified working syntax, with a fallback for subscribers who don't have `first_name` set: + +``` +{{ if .Subscriber.Attribs.first_name }}Hi {{ .Subscriber.Attribs.first_name }},{{ else }}Hi there,{{ end }} +``` + +**Don't trust listmonk's `/api/campaigns/:id/preview` endpoint (or its admin-UI "Preview" button) to test this** — it always renders against listmonk's built-in demo subscriber (`demo@listmonk.app`, attribs `{city: Bengaluru}`), ignoring which subscriber you're looking at, so `first_name` will always look unset there even when it's really set on a real subscriber. Use `campaigns test SLUG EMAIL...` (a real test-send to a real subscriber's own address) to verify personalization actually renders — that's how the syntax above was confirmed. + ## Segmentation `segment_query` is a raw Postgres-style SQL boolean expression — the same mechanism listmonk's own admin UI search box accepts. Since a campaign can only target whole list(s), `sync` materializes the query into a managed list: