One-shot bilingual intro paragraph with markdown + author avatar

- 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.
This commit is contained in:
2026-05-15 15:32:30 +02:00
parent 9ccc636105
commit 9458867d4d
5 changed files with 258 additions and 11 deletions
+52 -1
View File
@@ -5,8 +5,10 @@ declare(strict_types=1);
use Fisharebest\Webtrees\Date;
use Fisharebest\Webtrees\Fact;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Http\RequestHandlers\TreePage;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Registry;
use Fisharebest\Webtrees\Tree;
use Illuminate\Support\Collection;
@@ -22,6 +24,8 @@ use Illuminate\Support\Collection;
* @var array<string,string> $relationships xref => "your mother" etc. (per-recipient)
* @var array<string,true> $detailed_xrefs xref-set — render in detail; others as summary bullet
* @var string $account_url
* @var string $intro Admin-supplied one-shot intro paragraph; "" = skip block
* @var Individual|null $intro_author Tree contact's linked record, if any — avatar source for the intro
*/
// ─── BockenTheme light-mode palette ─────────────────────────────────────
@@ -592,7 +596,20 @@ $timeline_arrow_row = '<tr>'
<h1 style="margin:10px 0 6px;font-weight:300;font-size:38px;line-height:1.1;letter-spacing:-0.015em;color:<?= $palette['ink'] ?>;">
<?= e($tree->title()) ?>
</h1>
<div style="font-size:13px;font-weight:300;color:<?= $palette['ink3'] ?>;">
<?php
// Strip the leading scheme so the link
// reads as a clean hostname/path — the
// anchor still points at the absolute URL.
$tree_url = route(TreePage::class, ['tree' => $tree->name()]);
$tree_url_lbl = preg_replace('~^https?://~i', '', rtrim($tree_url, '/'));
?>
<div style="margin-top:4px;font-size:13px;font-weight:400;letter-spacing:0.01em;">
<a href="<?= e($tree_url) ?>"
style="color:<?= $palette['link'] ?>;text-decoration:none;border-bottom:1px solid <?= $palette['link'] ?>33;">
<?= e($tree_url_lbl) ?>
</a>
</div>
<div style="margin-top:6px;font-size:13px;font-weight:300;color:<?= $palette['ink3'] ?>;">
<?= e($masthead_date($generated_at)) ?>
<span style="color:<?= $palette['mute'] ?>;">·</span>
<?= e(I18N::translate('Events in the next %d days.', $window_days)) ?>
@@ -600,6 +617,40 @@ $timeline_arrow_row = '<tr>'
</td>
</tr>
<?php if (trim($intro) !== '') : ?>
<!-- Editorial: one-shot intro paragraph ──────────── -->
<?php
// Render via webtrees' Markdown factory: CommonMark
// with autolinks, the same flavour used elsewhere
// in the site. Raw HTML in the source is escaped
// by the factory's HtmlFilter::ESCAPE setting, so
// a stray "<" can't break the email layout.
$intro_html = Registry::markdownFactory()->markdown(trim($intro), $tree);
$intro_inner = '<div style="border-left:3px solid ' . $palette['accent'] . ';padding:6px 0 6px 16px;'
. 'font-size:15px;line-height:1.55;font-weight:300;color:' . $palette['ink'] . ';'
. 'font-style:italic;">' . $intro_html . '</div>';
?>
<tr>
<td style="padding:0 8px 24px;">
<?php if ($intro_author instanceof Individual) : ?>
<table role="presentation" cellpadding="0" cellspacing="0" border="0"
style="width:100%;border-collapse:collapse;">
<tr>
<td style="width:72px;vertical-align:top;padding-top:4px;">
<?= $avatar($intro_author) ?>
</td>
<td style="vertical-align:top;">
<?= $intro_inner ?>
</td>
</tr>
</table>
<?php else : ?>
<?= $intro_inner ?>
<?php endif ?>
</td>
</tr>
<?php endif ?>
<?php if (!$birthdays->isEmpty()) : ?>
<?php
$detailed = [];