- Admin can set a per-locale intro paragraph for the next issue on
the preferences page; cleared automatically after a successful
send. Stored in module_setting (longText) so multi-paragraph
notes fit.
- Intro is rendered via webtrees' CommonMark factory (same flavour
as notes) with raw HTML escaped, supports {{first_name}},
{{last_name}}, {{username}}, {{email}} substitution per recipient.
- Two-column intro layout: tree contact user's linked Individual
becomes the editorial portrait on the left. Their avatar is
added to the per-recipient embed set so the inline image always
resolves rather than falling through to a tree-page login link.
- Masthead now shows the tree URL under the title.
- Avatar source dimensions bumped 96→192 px and JPEG quality 75→88
so portraits stay crisp at retina display ratios.
- Admin preferences page can now subscribe existing webtrees users
per tree, not just external addresses.
- Subject prefix is now configurable per locale (en/de), and the
date in the subject is formatted via IntlDateFormatter in the
recipient's locale.
- "From:" header now uses SiteUser (SMTP_FROM_NAME/SMTP_DISP_NAME)
to match webtrees' own system-mail convention; the tree contact
becomes the Reply-To.
Admin-facing simplification:
- Dropped separate \"lookahead\" and \"historical lookahead\" tree
prefs (and the once-per-month historical gate). A single
\"send every N days\" number now drives both the cron cadence
and the window each issue looks ahead for living + deceased
events.
- Default 14, range 1–90, applies uniformly.
User-facing addition:
- The /my-account/{tree} subscription card gained an \"Email
frequency\" select with options: use site default, weekly,
every 2 weeks, monthly, every 2 months, quarterly. Stored as
a per-tree-per-user preference.
- Dispatch now checks each recipient's own cadence against
their own last-sent timestamp. Admin-added external addresses
with no webtrees account always receive every run (no
per-user state).
- Newsletter footer now reads \"You can change how often you
receive this email, or unsubscribe entirely, in the Newsletter
subscription section on your My account page\" — true now
that the control exists.
German translations updated for the new strings; stale ones
removed.
The fixed 140px width on the timeline TD reserved a lot of empty
space to the right of the date — on narrow viewports it squeezed
the cards severely. Replaced the hard width with the standard
\`width:1%\` email-table trick so the cell collapses to whatever
the date line actually needs (about 80-95px); the card column
inherits the rest.
Right padding dropped from 14 to 4 to tighten further.
- Two new stub rows now bookend each section's rail. The top
cap is a 4px-high TR whose rail TD carries border-top-left/
-right-radius so the rail's top end visually rounds off.
The bottom cap is an 18px-high TR with border-bottom-left/
-right-radius so the rail's tail tapers into a rounded stop
beneath the last card.
- The chevron arrowhead now lives in its own TR with no rail
border, sitting 14px below the rounded rail-end and another
~18px of cell height below that — visibly separated from the
rail rather than sitting flush against its tip.
- Chevron's horizontal offset shifted from -38 to -37 (one px
right) and SVG width bumped to 26 to give the arrow a little
more visual weight at its new lower position.
- Bumped the dot's margin-left from -31 to -33 so its centre
lands on the rail's centre (the border-collapsed 4px border
effectively leaves 26px between rail centre and content-start;
half of a 14px dot is 7px, so margin = -(26+7)). margin-right
bumped to 16 to preserve the gap to the date text.
- Replaced the CSS-triangle arrowhead with an inline SVG chevron
(stroke-linecap:round, stroke-linejoin:round, stroke-width:3)
so the cap matches the rounded "v" arrow look the user asked
for. SVG sits at the bottom of its cell with the same -38
offset as the dot pile so dot, chevron and rail share a single
vertical axis.
The previous border-spacing approach made each card stand on its
own — but it also fragmented the rail. Each rail segment ended
at the bottom of its row, leaving visible breaks between cards.
Restructured each event row to wrap the avatar+content cells in
a nested card table sitting in the row's left outer TD. The
right TD carries the rail as its border-left. With the outer
section table now using border-collapse:collapse, consecutive
rows' left-borders touch and merge into one unbroken line that
runs through the card gaps.
Added a downward triangle TR after the last event of each
section as a visual cap on the rail. Pure CSS (border-style
trick), coloured to match the rail.
- Each event row is now a self-contained card (full border on
all four sides, rounded all four corners). border-spacing on
the section table inserts a 10px vertical gap between cards
so they read as distinct entries. The timeline rail breaks
along with the cards, which actually reads better — each event
feels like its own beat on the timeline rather than rungs of a
ladder.
- When two events share an upcoming day, the second row
(dotless) used to repeat "17. MAI" — wasted vertical space.
It now only prints the year, bumped up to 14px so it carries
on its own. The always-visible year is also slightly larger
(13px vs 11px) and tonally lifted from #aaa muted to #777
tertiary so it pairs evenly with the day-month line above it.
Addresses feedback that the previous timeline competed for
attention with the year and felt visually trapped inside the
event card.
- The card surface (cream background, hairline border, rounded
corners) is now built per-row from the avatar + content TDs.
The timeline TD sits on the page background to the right of
the card with a 16px gutter between them.
- Rail bumped from 1px to 4px, in a warm grey #cdc7be that
reads as a deliberate ribbon rather than a divider.
- Dots are 14px (up from 10), with a 4px page-coloured halo so
they punch through the rail. Single colour (nord10 blue) for
every event — no more per-event-type tinting.
- Each calendar day shows exactly one dot: rows are walked in
upcoming-anniversary order and any row whose $fact->jd matches
the previous row renders without a dot but keeps its date
text, so two May-17 deaths share one marker on the rail.
- Date display split into "17. MAI" (semi-bold 13px tracking)
and "1759" (light 11px, muted) on a second line, so the day
of the year reads as the primary axis and the year as
supporting context.
- Relationship label moved from inline "(your great-aunt)" to a
separate italic muted line beneath the name, so long
relationship strings don't crowd the event label.
Replaces the previous "depth in generations along the strict
lineal chain" definition (which excluded siblings, aunts, cousins
entirely) with the metric the user actually wants: the number of
descent-steps separating the target from the recipient's closest
direct ancestor or descendant.
Examples relative to the recipient:
- sibling: 1 (parent → sibling)
- great-aunt: 1 (great-grandparent → great-aunt)
- nephew: 2 (parent → sibling → nephew)
- first cousin: 2 (grandparent → aunt → cousin)
- second cousin: 3
- ego, parents, grandparents, ..., children, ..., great-greats: 0
- own spouse, step-parents, brothers-in-law: inherit partner's
distance (so spouse-of-distance-1 is also distance 1)
Implementation:
- Anchor set seeded with R's direct ancestors + R + direct
descendants (capped at 25 generations to bound runaway data).
- Multi-source BFS expanding by descent only.
- Spouse propagation at every level so a person and their
spouse always share the same distance.
- Memoised per (recipient xref, max distance).
Tree preference key and range kept (NEWSLETTER_LINEAL_DEPTH,
0–10, default 3); only the semantics and the user-facing label
+ help text change, with concrete examples in both English and
German.
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.").
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").