Per-recipient: only direct ancestors and direct descendants
within a configurable number of generations (default 3) get the
full row treatment (avatar, icon, timeline). Everyone else falls
through to a compact text-only bullet list at the bottom of the
same section.
- New tree preference NEWSLETTER_LINEAL_DEPTH (range 0–10,
default 3) with a clearly-explained admin input.
- RelationshipPathFinder::linealKin() does two cheap recursive
expansions (ancestors and descendants only — no spouse or
sibling traversal) and returns the xref set. Memoised per
recipient within a dispatch run.
- Avatar attachments are filtered per recipient to only the
embeds actually referenced in their HTML, so summary-only rows
no longer inflate per-email size with unused images.
- Recipients with no PREF_TREE_ACCOUNT_XREF (external admin
addresses, users not linked to a record) see the entire
newsletter in detail — no lineal anchor to filter against.
- German translations for the three new section kickers ("Other
birthdays", etc.) and the admin input help text.
Each featured person now carries a parenthetical label relative
to the recipient: "Jane Doe (your mother) — 45th birthday",
"Karl Müller (your 4th great-grandfather) — death". Labels are
italic, muted, and only appear when a path can be computed.
- New RelationshipPathFinder service mirrors webtrees'
RelationshipService::getCloseRelationship BFS but with a
configurable depth (default 14 hops ≈ 7 generations) so it
reaches great-great-grandparents and beyond. Results are
memoised per (recipient xref, target xref) within one
dispatch run.
- nameFromPath() formatting is delegated to webtrees so the
label honours the configured UI language (German, English,
etc.) and gendered/inflected forms.
- The recipient's tree-bound Individual is looked up via
Tree::getUserPreference(user, PREF_TREE_ACCOUNT_XREF). External
admin-added recipients (no webtrees account, no linked record)
silently get no labels — names render plain.
- Trade-off: the view now renders once per recipient (instead of
once per language group), because the relationship map is
personalised. For typical subscriber counts the extra string-
concat cost is negligible compared to the SMTP send itself.
Drops the editorial-serif palette in favour of the actual
website tokens so the newsletter and the site read as one
product:
- Cream #f8f6f1 page, surface #efecea card patches with #ddd
hairline borders matching the .card component.
- Open Sans 300/400/500/600 throughout (Google Fonts @import,
Helvetica/Arial fallback).
- Nord accent colours: nord10 blue for birth + links, nord3
graphite for death, nord15 mauve for marriage anniversaries.
Each event row's timeline dot is colour-coded to its event
type, which gives the right-hand rail a quiet ribbon of
meaning when several event types appear in one card.
- Soft elevation on cards (1px shadow), thin underlines on
links — same affordance the site uses.
- Header/footer chrome simplified: small caps kicker in nord10,
light-weight site title, no ornaments.
Reworks the newsletter as a family-chronicle layout: ivory paper
background, deep oxblood ink, aged-gold accents, EB Garamond
display with Georgia body fallback.
- Inline SVG event icons (sparkle for birth, dagger for death,
interlocked rings for marriage). Falls back silently in
Outlook desktop; modern Gmail / Apple / iOS / Outlook 365
render them.
- Right-side gold hairline timeline running through the date
column of every event row, with a filled dot per entry.
- Person names link to their webtrees Individual page via
Individual::url() (absolute URL through route() → BASE_URL),
including the avatar circles.
- German strings added for the new section kickers
("Family Chronicle", "Living kin who will celebrate this
fortnight.", "Marriages still intact.").
Source media files can easily be multiple megabytes — embedding
the originals made a per-recipient email balloon to 10MB+. Each
avatar is now cover-cropped to 96x96 (HiDPI for the rendered
48px circle) and re-encoded as JPEG q=75 via Intervention\Image,
which webtrees already depends on. Typical avatar payload drops
from megabytes to ~5-15KB.
Falls back to the original bytes (with a log warning) if neither
Imagick nor GD is loaded — better an oversized email than none.
The "once-per-calendar-month" gate that prevents the historical
section from appearing on every regular send also suppressed it
on admin "Send now" previews after the first run of the month —
making the section silently disappear when re-testing the email.
Force-send now bypasses the gate but still updates the
last-historical-month stamp, so the real monthly cadence stays
intact for cron-driven sends.
Pull each individual's highlighted media image via webtrees'
Individual::findHighlightedMediaFile, attach as Symfony inline
parts with stable cid:avatar-<xref> identifiers, and render
border-radius:50% on the <img>. Couples on anniversaries show
both spouses' circles side-by-side.
Fallback when no image is available (privacy-hidden record, no
OBJE, external URL, unreadable file): a CSS-only coloured circle
with the person's initials. The hue is derived from a hash of
the XREF so the same person keeps the same colour across
newsletters.
Done via a NewsletterMailer subclass of EmailService that adds a
sendWithEmbeds() method — the parent's transport() and DKIM
config still apply, only the message-construction path differs.
Recurring email newsletter for webtrees 2.2+. Each enabled tree
sends upcoming birthdays of living individuals, optional marriage
anniversaries of intact couples, and a once-per-calendar-month
historical section of births and deaths of deceased individuals.
Triggered exclusively by an external scheduler (system cron,
systemd timer, etc.) hitting a token-gated HTTP endpoint — never
on visitor page loads. The "is it due?" decision is idempotent
within the configured frequency window.
Per-user subscription is integrated into the built-in
/my-account/{tree} page via a custom view + a decorated
AccountUpdate handler. Admins can add external addresses and
trigger an immediate send for testing. Email body renders in
German for German-language users; English otherwise. Birthdays
and anniversaries are formatted with the upcoming-event ordinal
age (e.g. "45th birthday" / "45. Geburtstag").