Skip to content

Add a "View in browser" link to broadcast and automation emails - #502

Open
pierre-b wants to merge 9 commits into
devfrom
feat/view-in-browser
Open

pierre-b wants to merge 9 commits into
devfrom
feat/view-in-browser

Conversation

@pierre-b

@pierre-b pierre-b commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Implements point 1 of #441.

What it does

  • Templates can use {{ view_in_browser_url }}. Broadcasts (all three send paths) and automations fill it with <endpoint>/v/<token>. Transactional and double opt-in emails leave it empty.
  • The token is a tracking token with a new purpose, v. It is signed with the workspace key, bound to the workspace and the message, and has no unsigned or legacy form.
  • GET /v/<token> verifies the token, then re-renders the template version recorded for that message, using the data recorded in message history. Tracking is off: no pixel, no /r/ redirect, no identity token. Links keep the UTM parameters of the send.
  • A web version is served only when the email content (the MJML source in code mode, or the visual tree) uses the variable. Otherwise the page answers 404.
  • Automation emails now record their template version and render data in message history (encrypted), as broadcasts already do.
  • The page answers 404 in these cases, all with the same body:
    • the token does not verify
    • the message is not a broadcast or automation email
    • the contact was deleted
    • the template version is missing
    • the template was deleted
    • the template does not use the variable
  • /v/preview (the sample link in template previews and test emails) answers with a page that explains it.

Security

  • Every response, error pages included, carries a fixed CSP: sandbox allow-popups allow-popups-to-escape-sandbox allow-downloads, default-src 'none', form-action 'none', frame-ancestors 'none', base-uri 'none'.
    • Why: contact fields go into the HTML unescaped, and the page is served on the console's origin, where the console keeps its session. The sandbox stops scripts and gives the page an opaque origin.
  • The page also sends no-referrer, private, no-store, noindex, nofollow and nosniff.
  • Rate limits: 300 requests per minute per address, then 30 views per minute per message. The per-message limit applies only once the token verifies, so a forged token cannot use up a real message's budget.
  • /v/ requests are not traced, so tokens never reach the trace backend.
  • Viewing writes nothing: there are only reads.

Console

  • The link editor of the visual builder has a new link type, "Web version of the email". It inserts the variable and recognises it when the link is reopened.
  • The code editor suggests the variable, and the AI assistant prompt documents it.
  • New templates get a sample value in their test data.
  • The 3 new strings are translated in all 8 catalogues.

Release

  • 43.1 (minor, no schema change), with a CHANGELOG entry. The web analytics SDK version is synced.
  • Upgrade note: on a blog's custom domain, a category with the slug v is now shadowed by /v/.

Tests

  • Unit tests: token, BuildTemplateData, each send path, the repository read, the service, the handler, route wiring and tracing.
  • Integration (TestWebVersion):
    • a broadcast is viewed with nothing recorded, and its /r/ link redirects to /v/
    • markup in a first name stays under the sandbox
    • the page shows the version that was sent
    • deleting the template takes the page down
    • deleting the contact takes the page down
    • an automation email has a web version
    • a template without the variable has no web version
    • transactional emails get no link
    • a forged token gets 404
  • Runs: go test ./internal/... ./pkg/... and the full integration suite pass. The console suite passes (npx vitest run, 2,100 tests), as do tsc -b and eslint.

Known limits, not addressed here

  • The language variant is resolved again with the workspace's current default language. If the default changes after the send, a contact whose language matches the new default sees the default content.
  • A token renders on any host of the instance, including another workspace's custom domain.
  • A history write that fails after delivery leaves a link that answers 404. This is the existing worker behaviour.

Docs: Notifuse/docs#25.

Sign a web version token per message (new trackingtoken purpose) and expose it
to templates as view_in_browser_url for broadcasts and automations. Outside that
scope the variable renders empty, and a value supplied by the caller is kept.

Automation emails now record their template version and template data in
message history, like broadcasts.
The link editor gets a "Web version of the email" link type that inserts
{{ view_in_browser_url }} and recognises it when reopened, the code editor
suggests the variable, the AI assistant prompt documents it, and the default
test data carries a sample address.
Serve a web version only when the email content uses the variable, keep
/v/ requests out of traces, allow downloads under the sandbox, answer the
preview link with a page that explains it, log abandoned requests at debug
level, and document the guard with blank and the reserved /v/ path.
@codecov

codecov Bot commented Nov 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.22642% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.76%. Comparing base (c859e1c) to head (b729607).

Files with missing lines Patch % Lines
internal/service/web_version_service.go 93.27% 4 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #502      +/-   ##
==========================================
+ Coverage   88.73%   88.76%   +0.03%     
==========================================
  Files         365      367       +2     
  Lines       71197    71409     +212     
==========================================
+ Hits        63174    63384     +210     
  Misses       5397     5397              
- Partials     2626     2628       +2     
Flag Coverage Δ
unit 88.76% <96.22%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant