diff --git a/.changeset/cyan-ears-shout.md b/.changeset/cyan-ears-shout.md
new file mode 100644
index 0000000000..1e016cc557
--- /dev/null
+++ b/.changeset/cyan-ears-shout.md
@@ -0,0 +1,5 @@
+---
+'hive': patch
+---
+
+Standardize the design and content of all email templates for consistency.
diff --git a/packages/services/emails/src/mjml.ts b/packages/services/emails/src/mjml.ts
index cbcbd5b9bb..87b789d157 100644
--- a/packages/services/emails/src/mjml.ts
+++ b/packages/services/emails/src/mjml.ts
@@ -8,7 +8,7 @@ type RawValue = {
readonly content: string;
};
type SpecialValues = RawValue;
-type ValueExpression = string | SpecialValues;
+type ValueExpression = string | SpecialValues | MJMLValue;
export function mjml(parts: TemplateStringsArray, ...values: ValueExpression[]): MJMLValue {
let content = '';
@@ -29,6 +29,8 @@ export function mjml(parts: TemplateStringsArray, ...values: ValueExpression[]):
content += token.content;
} else if (typeof token === 'string') {
content += escapeHtml(token);
+ } else if (token.kind === 'mjml') {
+ content += token.content;
} else {
throw new TypeError('mjml: Unexpected value expression.');
}
diff --git a/packages/services/emails/src/templates/audit-logs-report.ts b/packages/services/emails/src/templates/audit-logs-report.ts
index 399123b39b..63b9c7e70e 100644
--- a/packages/services/emails/src/templates/audit-logs-report.ts
+++ b/packages/services/emails/src/templates/audit-logs-report.ts
@@ -1,4 +1,4 @@
-import { mjml } from '../mjml';
+import { button, email, mjml, paragraph } from './components';
export function renderAuditLogsReportEmail(input: {
organizationName: string;
@@ -6,22 +6,13 @@ export function renderAuditLogsReportEmail(input: {
formattedEndDate: string;
url: string;
}) {
- return mjml`
-
-
-
-
-
-
-
- Audit Logs for your organization ${input.organizationName} from ${input.formattedStartDate} to ${input.formattedEndDate}
- .
-
- Download Audit Logs CSV
-
-
-
-
-
- `.content;
+ return email({
+ title: 'Your Requested Audit Logs Are Ready',
+ body: mjml`
+ ${paragraph(mjml`You requested audit logs for ${input.formattedStartDate} – ${input.formattedEndDate}, and they are now ready for download.`)}
+ ${paragraph('Click the link below to download your CSV file:')}
+ ${button({ url: input.url, text: 'Download Audit Logs' })}
+ ${paragraph(`If you didn't request this, please contact support@graphql-hive.com.`)}
+ `,
+ });
}
diff --git a/packages/services/emails/src/templates/components.ts b/packages/services/emails/src/templates/components.ts
new file mode 100644
index 0000000000..6194168b1f
--- /dev/null
+++ b/packages/services/emails/src/templates/components.ts
@@ -0,0 +1,52 @@
+import { mjml, type MJMLValue } from '../mjml';
+
+export { mjml };
+
+export function paragraph(content: string | MJMLValue) {
+ return mjml`
+
+ ${content}
+
+ `;
+}
+
+export function button(input: { url: string; text: string }) {
+ return mjml`
+
+ ${input.text}
+
+ `;
+}
+
+export function email(input: { title: string | MJMLValue; body: MJMLValue }) {
+ return mjml`
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+ ${input.title}
+
+ ${input.body}
+
+
+
+
+
+
+ © ${mjml.raw(String(new Date().getFullYear()))} Hive. All rights reserved.
+
+
+
+
+
+ `.content;
+}
diff --git a/packages/services/emails/src/templates/email-verification.ts b/packages/services/emails/src/templates/email-verification.ts
index ba46386544..806bb95525 100644
--- a/packages/services/emails/src/templates/email-verification.ts
+++ b/packages/services/emails/src/templates/email-verification.ts
@@ -1,923 +1,16 @@
-export function renderEmailVerificationEmail(params: {
+import { button, email, mjml, paragraph } from './components';
+
+export function renderEmailVerificationEmail(input: {
subject: string;
verificationLink: string;
toEmail: string;
}) {
- return `
-
-
-
-
-
-
-
- ${params.subject}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Please verify your email address for Hive
- by clicking the button below.
-
-
-
-
-
-
-
-
-
- |
-
-
-
- |
-
-
-
- |
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
-`;
+ return email({
+ title: `Verify Your Email Address`,
+ body: mjml`
+ ${paragraph(`To complete your sign-up, please verify your email address by clicking the link below:`)}
+ ${button({ url: input.verificationLink, text: 'Verify Email' })}
+ ${paragraph(`If you didn't sign up, you can ignore this email.`)}
+ `,
+ });
}
diff --git a/packages/services/emails/src/templates/organization-invitation.ts b/packages/services/emails/src/templates/organization-invitation.ts
index 88ec0900c2..5f89fc9dd4 100644
--- a/packages/services/emails/src/templates/organization-invitation.ts
+++ b/packages/services/emails/src/templates/organization-invitation.ts
@@ -1,20 +1,11 @@
-import { mjml } from '../mjml';
+import { button, email, mjml, paragraph } from './components';
export function renderOrganizationInvitation(input: { organizationName: string; link: string }) {
- return mjml`
-
-
-
-
-
-
- Someone from ${input.organizationName} invited you to join GraphQL Hive.
- .
-
- Accept the invitation
-
-
-
-
- `.content;
+ return email({
+ title: `Join ${input.organizationName}`,
+ body: mjml`
+ ${paragraph(mjml`You've been invited to join ${input.organizationName} on GraphQL Hive.`)}
+ ${button({ url: input.link, text: 'Accept the invitation' })}
+ `,
+ });
}
diff --git a/packages/services/emails/src/templates/organization-ownership-transfer.ts b/packages/services/emails/src/templates/organization-ownership-transfer.ts
index 556b5b52c5..3cc836cc4e 100644
--- a/packages/services/emails/src/templates/organization-ownership-transfer.ts
+++ b/packages/services/emails/src/templates/organization-ownership-transfer.ts
@@ -1,29 +1,18 @@
-import { mjml } from '../mjml';
+import { button, email, mjml, paragraph } from './components';
export function renderOrganizationOwnershipTransferEmail(input: {
authorName: string;
organizationName: string;
link: string;
}) {
- return mjml`
-
-
-
-
-
-
-
- ${input.authorName} wants to transfer the ownership of the ${input.organizationName} organization.
-
-
- Accept the transfer
-
-
- This link will expire in a day.
-
-
-
-
-
- `.content;
+ return email({
+ title: 'Organization Ownership Transfer Initiated',
+ body: mjml`
+ ${paragraph(
+ mjml`${input.authorName} wants to transfer the ownership of the ${input.organizationName} organization.`,
+ )}
+ ${button({ url: input.link, text: 'Accept the transfer' })}
+ ${paragraph(`This link will expire in a day.`)}
+ `,
+ });
}
diff --git a/packages/services/emails/src/templates/password-reset.ts b/packages/services/emails/src/templates/password-reset.ts
index 6db6316ca9..4f7b2c8423 100644
--- a/packages/services/emails/src/templates/password-reset.ts
+++ b/packages/services/emails/src/templates/password-reset.ts
@@ -1,921 +1,16 @@
-export function renderPasswordResetEmail(params: {
+import { button, email, mjml, paragraph } from './components';
+
+export function renderPasswordResetEmail(input: {
subject: string;
passwordResetLink: string;
toEmail: string;
}) {
- return `
-
-
-
-
-
-
-
- ${params.subject}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- A password reset request for your account on
- Hive has been received.
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
-
- |
-
-
-
- |
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
-`;
+ return email({
+ title: `Reset Your Password`,
+ body: mjml`
+ ${paragraph(`We received a request to reset your password. Click the link below to set a new password:`)}
+ ${button({ url: input.passwordResetLink, text: 'Reset your password' })}
+ ${paragraph(`If you didn't request a password reset, you can ignore this email.`)}
+ `,
+ });
}
diff --git a/packages/services/emails/src/templates/rate-limit-exceeded.ts b/packages/services/emails/src/templates/rate-limit-exceeded.ts
index edaae6eb87..122ae81216 100644
--- a/packages/services/emails/src/templates/rate-limit-exceeded.ts
+++ b/packages/services/emails/src/templates/rate-limit-exceeded.ts
@@ -1,4 +1,4 @@
-import { mjml } from '../mjml';
+import { button, email, mjml, paragraph } from './components';
const numberFormatter = new Intl.NumberFormat();
@@ -8,30 +8,18 @@ export function renderRateLimitExceededEmail(input: {
currentUsage: number;
subscriptionManagementLink: string;
}) {
- return mjml`
-
-
-
-
-
-
-
- Your Hive organization ${
- input.organizationName
- } has reached over 100% of the operations limit quota.
- Used ${numberFormatter.format(input.currentUsage)} of ${numberFormatter.format(
- input.limit,
- )}.
- .
-
- We recommend to increase the limit.
-
-
- Manage your subscription
-
-
-
-
-
- `.content;
+ return email({
+ title: 'Rate Limit Reached',
+ body: mjml`
+ ${paragraph(
+ mjml`Your Hive organization ${
+ input.organizationName
+ } has reached over 100% of the operations limit quota.. Used ${numberFormatter.format(input.currentUsage)} of ${numberFormatter.format(
+ input.limit,
+ )}.`,
+ )}
+ ${paragraph(`We recommend to increase the limit.`)}
+ ${button({ url: input.subscriptionManagementLink, text: 'Manage your subscription' })}
+ `,
+ });
}
diff --git a/packages/services/emails/src/templates/rate-limit-warning.ts b/packages/services/emails/src/templates/rate-limit-warning.ts
index d5adf6d0b2..d1fd137ffd 100644
--- a/packages/services/emails/src/templates/rate-limit-warning.ts
+++ b/packages/services/emails/src/templates/rate-limit-warning.ts
@@ -1,4 +1,4 @@
-import { mjml } from '../mjml';
+import { button, email, mjml, paragraph } from './components';
const numberFormatter = new Intl.NumberFormat();
@@ -8,30 +8,18 @@ export function renderRateLimitWarningEmail(input: {
currentUsage: number;
subscriptionManagementLink: string;
}) {
- return mjml`
-
-
-
-
-
-
-
- Your Hive organization ${
- input.organizationName
- } is approaching its operations limit quota.
- Used ${numberFormatter.format(input.currentUsage)} of ${numberFormatter.format(
- input.limit,
- )}.
- .
-
- We recommend to increase the limit.
-
-
- Manage your subscription
-
-
-
-
-
- `.content;
+ return email({
+ title: 'Approaching Rate Limit',
+ body: mjml`
+ ${paragraph(
+ mjml`Your Hive organization ${
+ input.organizationName
+ } is approaching its operations limit quota. Used ${numberFormatter.format(input.currentUsage)} of ${numberFormatter.format(
+ input.limit,
+ )}.`,
+ )}
+ ${paragraph(`We recommend to increase the limit.`)}
+ ${button({ url: input.subscriptionManagementLink, text: 'Manage your subscription' })}
+ `,
+ });
}