| \n \n \n\n\n \n \n\n\n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n
| \n
| \n \n \n\n\n \n \n\n\n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n
| \n
+
+ {{#view_url}}
+
+ {{/view_url}}
+
diff --git a/scripts/campaigns/templates/gift-email-fr.json b/scripts/campaigns/templates/gift-email-fr.json
index e274ff6..7ba76f9 100644
--- a/scripts/campaigns/templates/gift-email-fr.json
+++ b/scripts/campaigns/templates/gift-email-fr.json
@@ -20,7 +20,7 @@
"id": "HTML-1",
"type": "html",
"values": {
- "html": "\n \n \n \n
\n \n ",
+ "html": "\n \n \n \n
\n \n ",
"hideDesktop": false,
"displayCondition": null,
"containerPadding": "0px",
diff --git a/services/targo-hub/lib/campaigns.js b/services/targo-hub/lib/campaigns.js
index 0135857..6dd7f49 100644
--- a/services/targo-hub/lib/campaigns.js
+++ b/services/targo-hub/lib/campaigns.js
@@ -809,6 +809,12 @@ async function sendCampaignAsync (id) {
const lang = (r.language || 'fr').toLowerCase().split('-')[0]
const tplText = getTpl(lang)
+ // Web fallback ("View in browser") so recipients with rendering
+ // issues (image-blocking, antique Outlook, third-party mail apps)
+ // can open the campaign in any modern browser. The URL hits the
+ // /recipients/:row_index/view endpoint defined further down which
+ // re-renders the same template with this recipient's variables.
+ const viewUrl = `${cfg.HUB_PUBLIC_URL || 'https://msg.gigafibre.ca'}/campaigns/${encodeURIComponent(id)}/recipients/${i}/view`
// Per-recipient amount override. Precedence:
// 1. r.amount — explicit override typed in the manual-add dialog
// 2. r.gift_value_cents → "$X" formatted (when CSV import set this)
@@ -836,6 +842,7 @@ async function sendCampaignAsync (id) {
expiry: p.expiry || '',
commitment_months: p.commitment_months || '3',
year: new Date().getFullYear(),
+ view_url: viewUrl,
}
const html = renderTemplate(tplText, vars)
const toName = `${r.firstname || ''} ${r.lastname || ''}`.trim()
@@ -1535,6 +1542,60 @@ async function handle (req, res, method, path) {
// ── Per-campaign wildcard routes (MUST stay below the /templates and
// /webhook fixed paths above, otherwise the wildcard captures them) ─────
+ // GET /campaigns/:id/recipients/:i/view — re-render the email for ONE
+ // recipient using the same variables the worker used at send time. Linked
+ // from the "Voir dans le navigateur / View in browser" line at the top of
+ // every campaign email so recipients with rendering issues (image-blocking
+ // clients, antique Outlook, niche third-party mail apps) can fall back to
+ // a fresh browser render. No auth needed — the campaign-id is 21-char
+ // nanoid (≈10²¹ space) and row_index alone is enough to reconcile.
+ const viewMatch = path.match(/^\/campaigns\/([^/]+)\/recipients\/(\d+)\/view$/)
+ if (viewMatch && method === 'GET') {
+ const c = loadCampaign(viewMatch[1])
+ if (!c) return json(res, 404, { error: 'not found' })
+ const i = parseInt(viewMatch[2], 10)
+ const r = (c.recipients || [])[i]
+ if (!r) return json(res, 404, { error: 'recipient not found' })
+ const p = c.params || {}
+ const lang = (r.language || 'fr').toLowerCase().split('-')[0]
+ const tplPath = p.template_path || templateForLanguage(lang)
+ let tplText
+ try { tplText = fs.readFileSync(tplPath, 'utf8') }
+ catch { return json(res, 500, { error: 'template missing' }) }
+ let displayAmount = p.amount || '50 $'
+ if (r.amount) {
+ displayAmount = r.amount
+ } else if (r.gift_value_cents) {
+ const cents = Number(r.gift_value_cents) || 0
+ displayAmount = cents % 100 === 0
+ ? `${cents / 100} $`
+ : `${(cents / 100).toFixed(2)} $`
+ }
+ const vars = {
+ firstname: r.firstname || (lang === 'en' ? 'dear customer' : 'cher client'),
+ lastname: r.lastname || '',
+ email: r.email,
+ description: r.civic_address || '',
+ gift_url: r.gift_url,
+ amount: displayAmount,
+ expiry: p.expiry || '',
+ commitment_months: p.commitment_months || '3',
+ year: new Date().getFullYear(),
+ // Empty so the {{#view_url}} section block in the template collapses
+ // — we don't want the "view in browser" line to show up when the
+ // user is ALREADY viewing it in a browser.
+ view_url: '',
+ }
+ const html = renderTemplate(tplText, vars)
+ res.writeHead(200, {
+ 'Content-Type': 'text/html; charset=utf-8',
+ 'Cache-Control': 'no-store',
+ // Disallow indexing — these URLs aren't meant for search engines
+ 'X-Robots-Tag': 'noindex, nofollow',
+ })
+ return res.end(html)
+ }
+
// GET /campaigns/:id/report.csv — per-recipient report download
// Columns chosen for operational follow-up (resend, refund, support):
// row, firstname, lastname, email, phone, language, customer_id, civic_address,
diff --git a/services/targo-hub/templates/gift-email-en.html b/services/targo-hub/templates/gift-email-en.html
index ba956d7..02e9db1 100644
--- a/services/targo-hub/templates/gift-email-en.html
+++ b/services/targo-hub/templates/gift-email-en.html
@@ -107,6 +107,13 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
| \n \n \n\n\n \n \n\n\n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n
| \n
| \n \n \n\n\n \n \n\n\n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n
| \n
+
+ {{#view_url}}
+
+ {{/view_url}}
+
diff --git a/services/targo-hub/templates/gift-email-en.json b/services/targo-hub/templates/gift-email-en.json
index 960e6a7..1706418 100644
--- a/services/targo-hub/templates/gift-email-en.json
+++ b/services/targo-hub/templates/gift-email-en.json
@@ -20,7 +20,7 @@
"id": "HTML-1",
"type": "html",
"values": {
- "html": "\n \n \n \n
\n \n ",
+ "html": "\n \n \n \n
\n \n ",
"hideDesktop": false,
"displayCondition": null,
"containerPadding": "0px",
diff --git a/services/targo-hub/templates/gift-email-fr.html b/services/targo-hub/templates/gift-email-fr.html
index cfab143..1835aed 100644
--- a/services/targo-hub/templates/gift-email-fr.html
+++ b/services/targo-hub/templates/gift-email-fr.html
@@ -107,6 +107,17 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
| \n \n \n\n\n \n \n\n\n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n
| \n
| \n \n \n\n\n \n \n\n\n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n
| \n
+
+ {{#view_url}}
+
+ {{/view_url}}
+
diff --git a/services/targo-hub/templates/gift-email-fr.json b/services/targo-hub/templates/gift-email-fr.json
index e274ff6..7ba76f9 100644
--- a/services/targo-hub/templates/gift-email-fr.json
+++ b/services/targo-hub/templates/gift-email-fr.json
@@ -20,7 +20,7 @@
"id": "HTML-1",
"type": "html",
"values": {
- "html": "\n \n \n \n
\n \n ",
+ "html": "\n \n \n \n
\n \n ",
"hideDesktop": false,
"displayCondition": null,
"containerPadding": "0px",
| \n \n \n\n\n \n \n\n\n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n
| \n
| \n \n \n\n\n \n \n\n\n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n
| \n