@nxgt/mail-presets
| Install | |
|---|---|
npm install @nxgt/mail-presets |
|
| Latest Version: | 1.2.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 14, 2026 |
| Links: | GitHub · npm |
@nxgt/mail-presets
Sixteen ready transactional e-mails for a Maizzle 6
project: verify-email, reset-password, password-changed,
email-changed, confirm-new-email, account-deleted, sign-in-code, magic-link,
new-sign-in, two-factor-enabled, two-factor-disabled,
recovery-code-used, confirm-action, welcome, invitation and
invitation-accepted, written with @nxgt/mail-ui's
components and translated in en and fr. Your project builds them with its
own brand and theme, next to its own templates, and replaces any of them — a
whole template, or one message.
// maizzle.config.ts
import { defineMailConfig } from '@nxgt/mail-config';
import { i18n } from '@nxgt/mail-i18n';
import { presets } from '@nxgt/mail-presets';
import { ui, uiCatalogues } from '@nxgt/mail-ui';
const mails = presets();
export default defineMailConfig({
plugins: [
ui({ brand: { name: 'Acme', url: 'https://acme.example' } }),
i18n({
locales: ['en', 'fr'],
catalogues: [uiCatalogues, mails.catalogues],
templates: [mails.templates],
}),
],
});
maizzle build then writes dist/en/verify-email.html,
dist/fr/verify-email.html… each with its .txt, and every e-mail's
placeholders and subjects in dist/mail-manifest.json. See what they look
like in the built samples.
Each preset as it arrives, with the brand Acme, the default theme and example
values in its placeholders. Click one for full size; fr is the French build.
1.x. Semantic versioning: a breaking change waits for the next major, and the changelog says what each release changes.
Install
bun add @nxgt/mail-presets @nxgt/mail-i18n @nxgt/mail-ui @nxgt/mail-config @maizzle/framework @maizzle/tailwindcss vue
Peers, all required:
@nxgt/mail-i18n— builds the templates once per locale, from the folderpresets()names, with the messages it gives.@nxgt/mail-ui— the templates are written with itsNx*components, and use its sharedcommon.*messages.@maizzle/framework(^6.1.7) — Maizzle itself.@nxgt/mail-configand@maizzle/tailwindcss(^1.5.6) are what@nxgt/mail-uineeds, the latter as a direct dependency of your project (see its Setup).vue(^3.5) — the templates are Vue single-file components.typescript(6). Bundler resolution ("moduleResolution": "bundler") is what is supported and tested;nodenextis out of contract.
Runs wherever maizzle build does: Node ^22.22.3, ^24.15.0 or >=26 — the
range Maizzle 6's own dependencies require — or Bun. CI runs the tests on
Bun, and builds the starter under Node 22.22.3.
Your project still has a locales/<locale>.json per locale, {} if it
overrides nothing: @nxgt/mail-i18n requires it.
Exports
| Export | What it is |
|---|---|
presets(options?) |
{ templates, catalogues } for i18n({ templates, catalogues }): every preset, or the ones in only |
PRESETS |
The sixteen names, ['verify-email', 'reset-password', …], as a readonly tuple |
PresetName |
One of them, as a type |
presetCatalogues |
Every preset's messages, { en, fr }, before presets() keeps the ones asked for; frozen, every group in it |
TEMPLATES_DIR |
The absolute path of the package's emails/ folder |
Presets, PresetsOptions |
What presets() answers, and its options |
Usage
A few of them — only
import { presets } from '@nxgt/mail-presets';
const mails = presets({ only: ['verify-email', 'reset-password', 'magic-link'] });
// mails.templates → { dir: '/…/@nxgt/mail-presets/emails', emails: ['verify-email', 'reset-password', 'magic-link'] }
// mails.catalogues → { en, fr }, with presets.*, verify-email.*, reset-password.*, magic-link.* only
Pass both to i18n() as above: only those three are built, beside your own
templates. A wrong only is a bare TypeError when the config loads:
presets: only holds something that is not a preset — name one of verify-email, reset-password, …. See Wiring the presets.
The e-mails
Each one takes its values at send time as placeholders, listed in the manifest:
| Placeholders | Subject (en) |
|
|---|---|---|
verify-email |
expiresIn, link, name |
Confirm your e-mail address |
reset-password |
expiresIn, link, name |
Reset your password |
password-changed |
link, name |
Your password was changed |
email-changed |
link, name, newEmail |
Your e-mail address was changed |
confirm-new-email |
expiresIn, link, name |
Confirm your new e-mail address |
account-deleted |
expiresIn, link, name |
Your account was deleted |
sign-in-code |
code, expiresIn |
Your sign-in code: {{ code }} |
magic-link |
expiresIn, link |
Your sign-in link |
new-sign-in |
device, link, location, name, time |
New sign-in to your account |
two-factor-enabled |
link, name |
Two-factor authentication was turned on |
two-factor-disabled |
link, name |
Two-factor authentication was turned off |
recovery-code-used |
link, name, recoveryCodesLeft, when |
A recovery code was used on your account |
confirm-action |
code, expiresIn, link, name |
Your confirmation code |
welcome |
link, name |
Welcome, {{ name }} |
invitation |
expiresIn, inviter, link, organization |
{{ inviter }} invited you to join {{ organization }} |
invitation-accepted |
invitee, link, organization |
{{ invitee }} accepted your invitation to {{ organization }} |
link is a URL in each of them: the sender fills it with an http: or https: URL.
expiresIn is how long the link or the code stays valid, already written in
the recipient's language — '1 hour', '1 heure' — shown as
This link expires in {{ expiresIn }}. (This code expires in … for
sign-in-code and confirm-action, This invitation expires in … for invitation,
This restoration link expires in … for account-deleted). See The e-mails for what each one
says, in both locales, and every message key.
Sending one
At send time, with @nxgt/mail's renderer, reading your build:
import { type Mailer, pickLocale } from '@nxgt/mail';
import { createMailRenderer } from '@nxgt/mail/renderer';
import type { MailEmails } from './generated/mail'; // written by each build, git-ignored
export const mails = createMailRenderer<MailEmails>({ dir: 'dist' });
export async function sendReset(
mailer: Mailer,
user: { email: string; name: string; locale: string | null },
link: string,
): Promise<void> {
const locale = pickLocale(user.locale, mails.locales, 'en');
const hours = new Intl.NumberFormat(locale, { style: 'unit', unit: 'hour', unitDisplay: 'long' });
await mailer.send({
to: user.email,
from: 'noreply@acme.example',
// expiresIn: '1 hour' in en, '1 heure' in fr
...mails.render('reset-password', { name: user.name, link, expiresIn: hours.format(1) }, { locale }),
});
}
The server knows how long the token it made lives; the build does not, so
the sender writes the duration in the recipient's language. Leaving
expiresIn out does not compile against MailEmails, and throws
render: reset-password needs the variable expiresIn
untyped.
Replacing a template
<!-- emails/welcome.vue — built instead of the package's welcome -->
<template>
<NxLayout :preheader="t('welcome.preheader')">
<NxTypography variant="headline-small">{{ t('welcome.title', { brand: brand.name }) }}</NxTypography>
<NxTypography>{{ t('common.greeting', { name: placeholder('name') }) }}</NxTypography>
<NxButton :href="placeholder('link')">{{ t('welcome.action') }}</NxButton>
<NxTypography variant="caption">{{ t('welcome.help') }}</NxTypography>
</NxLayout>
</template>
A template in your emails/ with the name of a preset replaces it. It keeps
the preset's messages, so it only writes the keys it adds (welcome.help
here) in your locales/<locale>.json. To start from the package's, copy it
from TEMPLATES_DIR.
Overriding a message
// locales/en.json — every other message stays the package's
{ "verify-email": { "action": "Yes, this is my address" } }
Your catalogues go over the package's, key by key, in each locale. The
fr button stays Confirmer mon adresse. See
Wiring the presets.
Traps
Give i18n() both catalogues. The templates use @nxgt/mail-ui's
common.* messages: without uiCatalogues the build fails with
calls t('common.footer.why'), which is not a key of the catalogues, and
without mails.catalogues, with calls t('verify-email.preheader'), ….
catalogues: [uiCatalogues, mails.catalogues],
List ui() in plugins. The templates are made of its Nx* components,
installed under node_modules, where Maizzle resolves no tag: ui() resolves
them, and without it the build fails with i18n: en/verify-email.html is empty — a tag of its template resolved to no component; list the plugin that brings it, as ui().
Another locale writes the keys itself. The presets have en and fr
only: a project in de writes common.*, presets.* and the group of each
preset it builds in locales/de.json, or the build fails with
i18n: de: presets.link-fallback is missing — en, the fallback locale, has it.
See Another locale.
An en override keeps the template's arguments. en is the fallback
locale, and the template still passes {brand} to verify-email.body: an
override without it fails with passes {brand} to verify-email.body, which does not use it. Replace the template to change what it passes.
only keeps only those presets' messages. A template of yours named
like a preset left out of only writes all of its messages itself.
expiresIn is text, not a number. Pass '1 hour', not 1 or
3600: the renderer writes a number as is, so the e-mail would say
This link expires in 3600.
Three subjects hold placeholders. welcome, sign-in-code and
invitation put {{ name }}, {{ code }}, {{ inviter }} and
{{ organization }} in the subject: take the subject from the manifest and
fill it like the body.
The samples are not in the tarball. They are on GitHub, in
samples/.
Type safety, counted
5 plausible mistakes, 5 refused at compile time, each measured by a
@ts-expect-error in
test/types/refusals.ts
that fails the typecheck the moment it stops holding:
- A name in
onlythat is not a preset ('sign-in'). onlygiven one name rather than a list.onlygiven an empty list, rather than left out for every preset.- The list given as the options (
presets(['welcome'])) rather than inonly. i18n({ templates })given the folder alone (mails.templates.dir) rather than the wholemails.templates.
The same file holds the calls that must keep compiling: a refusal that refuses the correct call is a bug.
Documentation
- The guides — one page per area, with every option, e-mail, message and error.
- Troubleshooting — an error message, its cause and its fix.
- Roadmap — what is next, and what is deliberately not planned.
- Samples
— every preset built in
enandfr, with the brandAcme. - Vocabulary — the words these pages use, defined once.
Licence
MIT
Related Packages
An i18n plugin for a Maizzle project: ICU catalogues checked at build time, t()...
E-mail components for a Maizzle project in the style of @nxgt/material-vue: NxLa...
Version History
| Version | Released | License |
|---|---|---|
| 1.2.0 | MIT | |
| 1.1.1 | MIT | |
| 1.1.0 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT | |
| 0.4.4 | MIT | |
| 0.4.3 | MIT | |
| 0.4.2 | MIT | |
| 0.4.1 | MIT | |
| 0.4.0 | MIT | |
| 0.3.0 | MIT | |
| 0.2.0 | MIT | |
| 0.1.2 | MIT | |
| 0.1.1 | MIT | |
| 0.1.0 | MIT |