Angular
A standalone Angular component that fetches signed values from an Express route and renders a sanitized iframe URL.
An Angular app runs entirely in the browser, so signing happens in a separate Express server. The component fetches the signed values with HttpClient and marks the resulting URL as trusted, which Angular requires before it will bind an iframe src.
Before you start
Follow the first three steps of the quickstart: create a plugin, add localhost:4200 to its allowed domains, and copy the Plugin ID and Secret.
Configuration
MAILEDITOR_PLUGIN_ID=9f3c1e7a-4b21-4c8e-9a77-1d2f5b6c8e10
MAILEDITOR_PLUGIN_SECRET=your_plugin_secret
MAILEDITOR_EDITOR_BASE_URL=https://maileditor.net
MAILEDITOR_PLUGIN_VERSION=1.0.0Never put these values in environment.ts. Anything there ends up in the browser bundle.
Sign on the server
Install Express (npm install express) and add "type": "module" to package.json if it is not there already.
import express from 'express'
import { createHmac } from 'node:crypto'
const app = express()
app.get('/api/auth', (req, res) => {
const user = req.query.user
if (typeof user !== 'string' || user === '') {
return res.status(400).json({ error: 'user is required' })
}
const expires = Math.floor(Date.now() / 1000) + 300
const payload = `v1|${Buffer.byteLength(user)}:${user}|${expires}`
const signature = createHmac('sha256', process.env.MAILEDITOR_PLUGIN_SECRET)
.update(payload)
.digest('hex')
res.json({
externalUserId: user,
identityExpires: expires,
identitySignature: signature,
pluginId: process.env.MAILEDITOR_PLUGIN_ID,
pluginVersion: process.env.MAILEDITOR_PLUGIN_VERSION,
editorBaseUrl: process.env.MAILEDITOR_EDITOR_BASE_URL,
})
})
app.listen(3001)Start it with node --env-file=.env server.js. In a real app, take the user id from the caller's session instead of trusting the user query parameter.
Proxy /api to it during development so the page and the API share one origin:
{
"/api": {
"target": "http://localhost:3001",
"secure": false
}
}Run ng serve --proxy-config proxy.conf.json, or set proxyConfig under the serve target in angular.json.
Render the iframe
Register HttpClient once:
import { ApplicationConfig } from '@angular/core'
import { provideHttpClient } from '@angular/common/http'
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient()],
}Then the component:
import { Component, inject, signal } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
interface EditorAuth {
externalUserId: string
identityExpires: number
identitySignature: string
pluginId: string
pluginVersion: string
editorBaseUrl: string
}
@Component({
selector: 'app-editor',
standalone: true,
template: `
@if (src(); as src) {
<iframe
[src]="src"
title="Email editor"
allow="clipboard-write; fullscreen"
style="width: 100%; height: 100vh; border: 0"
></iframe>
}
`,
})
export class EditorComponent {
private http = inject(HttpClient)
private sanitizer = inject(DomSanitizer)
src = signal<SafeResourceUrl | undefined>(undefined)
constructor() {
this.http
.get<EditorAuth>('/api/auth', { params: { user: 'customer-user-4471' } })
.subscribe((auth) => {
const params = new URLSearchParams({
external_user_id: auth.externalUserId,
identity_expires: String(auth.identityExpires),
identity_signature: auth.identitySignature,
plugin_version: auth.pluginVersion,
})
const url = `${auth.editorBaseUrl}/plugin-editor/v1/${auth.pluginId}?${params}`
this.src.set(this.sanitizer.bypassSecurityTrustResourceUrl(url))
})
}
}bypassSecurityTrustResourceUrl is required: Angular refuses to bind a plain string to an iframe src. The URL is safe to trust here because it is built from values your own server returned.
Next steps
- Add
template_idto the query string to open one of the user's saved templates. See Embedding the editor. - If the iframe shows "Plugin editor unavailable", read
result.errorin thesessionresponse. See Error codes.

.png)