diff --git a/.contentrain/content/system/email-templates/en.json b/.contentrain/content/system/email-templates/en.json index edeedaae..db72b001 100644 --- a/.contentrain/content/system/email-templates/en.json +++ b/.contentrain/content/system/email-templates/en.json @@ -1,104 +1,240 @@ { "ecb03525d606": { - "body": "
You've been invited to collaborate on Contentrain Studio. Click the button below to accept the invitation and sign in.
| Accept Invitation |
This invitation link expires in 7 days. If you weren't expecting this email, you can safely ignore it.
", + "about": "your invitation to Contentrain Studio", + "cta_label": "Accept invitation", + "cta_url": "{url}", + "heading": "You're invited to Contentrain Studio", + "lead": "You've been invited to collaborate on Contentrain Studio. Accept the invitation to sign in.", "name": "Auth — Account Invitation", + "notes": "This invitation link expires in 7 days. If you weren't expecting this email, you can safely ignore it.", + "preheader": "Accept your invitation to Contentrain Studio.", "slug": "auth-invite", "subject": "You've been invited to Contentrain Studio" }, "fce263ebd447": { - "body": "Click the button below to sign in to Contentrain Studio. No password needed.
| Sign In |
This link expires in 1 hour and can only be used once. If you didn't request this email, you can safely ignore it.
", + "about": "a sign-in request for Contentrain Studio", + "cta_label": "Sign in", + "cta_url": "{url}", + "heading": "Sign in to Contentrain Studio", + "lead": "Use the button below to sign in. No password needed.", "name": "Auth — Magic Link Sign-in", + "notes": "This link expires in 1 hour and can only be used once. If you didn't request this email, you can safely ignore it.", + "preheader": "Your sign-in link for Contentrain Studio.", "slug": "auth-magic-link", "subject": "Sign in to Contentrain Studio" }, "invite-added": { - "body": "Hi,
{inviterName} added you to the {workspaceName} workspace on Contentrain Studio.
", + "about": "your Contentrain Studio workspace access", + "cta_label": "Open workspace", + "cta_url": "{workspaceUrl}", + "heading": "You've been added to {workspaceName}", + "lead": "**{inviterName}** added you to the **{workspaceName}** workspace on Contentrain Studio.", "name": "Workspace Invite — Added", + "preheader": "Open the workspace to get started.", "slug": "invite-added", "subject": "You've been added to {workspaceName} on Contentrain Studio" }, "invite-reminder": { - "body": "Hi,
This is a reminder that you have a pending invitation to the {workspaceName} workspace on Contentrain Studio.
", + "about": "your Contentrain Studio workspace invitation", + "cta_label": "Open workspace", + "cta_url": "{workspaceUrl}", + "heading": "Your invitation to {workspaceName} is waiting", + "lead": "This is a reminder that you have a pending invitation to the **{workspaceName}** workspace on Contentrain Studio.", "name": "Workspace Invite — Reminder", + "preheader": "Open the workspace to accept your invitation.", "slug": "invite-reminder", "subject": "Reminder: You've been invited to {workspaceName} on Contentrain Studio" }, "payment-failed": { - "body": "Hi,
We couldn't charge your payment method for the {planName} subscription on {workspaceName}.
You have until {gracePeriodEndsText} to update your payment details. If the charge doesn't go through by then, the workspace will be paused until the payment goes through. Your content is safe, and access comes back as soon as the charge succeeds.
| Update payment method |
If you think this is a mistake, check with your card issuer or reply to this email.
", + "about": "billing for your Contentrain Studio workspace", + "cta_label": "Update payment method", + "cta_url": "{billingUrl}", + "heading": "Payment failed for {workspaceName}", + "lead": "We couldn't charge your payment method for the **{planName}** subscription on **{workspaceName}**.", "name": "Payment — Failed", + "notes": "You have until **{gracePeriodEndsText}** to update your payment details. If the charge doesn't go through by then, the workspace will be paused until the payment goes through. Your content is safe, and access comes back as soon as the charge succeeds.\n\nIf you think this is a mistake, check with your card issuer or reply to this email.", + "preheader": "Update your payment method to keep the workspace active.", "slug": "payment-failed", "subject": "Action required: payment failed for {workspaceName}" }, "payment-recovered": { - "body": "Hi,
Good news — we've successfully charged your payment method for {workspaceName}. Your {planName} subscription is active again.
No action needed. Thanks for sticking with Contentrain Studio.
| View billing |
Hi,
Your {planName} subscription for {workspaceName} is now active at {planPrice}/month.
You'll be billed monthly until you cancel. You can manage your subscription anytime:
| Manage billing |
Questions? Just reply to this email.
", + "about": "billing for your Contentrain Studio workspace", + "cta_label": "Manage billing", + "cta_url": "{billingUrl}", + "heading": "Your {planName} plan is active", + "lead": "Your **{planName}** subscription for **{workspaceName}** is now active at {planPrice}/month.", "name": "Subscription — Activated", + "notes": "You'll be billed monthly until you cancel. You can manage your subscription anytime.\n\nQuestions? Just reply to this email.", + "preheader": "You'll be billed monthly until you cancel.", "slug": "subscription-activated", "subject": "Your {planName} plan is active on Contentrain Studio" }, "subscription-cancel-scheduled": { - "body": "Hi,
Your {planName} subscription for {workspaceName} is set to cancel. Everything stays available until {accessEndsText} — you keep what you paid for.
After that date the workspace moves to the free plan. Changed your mind? You can keep the subscription anytime before then.
| Keep my subscription |
If this was unexpected, reply to this email and we'll sort it out.
", + "about": "billing for your Contentrain Studio workspace", + "cta_label": "Keep my subscription", + "cta_url": "{billingUrl}", + "heading": "Your {planName} plan stays active until {accessEndsText}", + "lead": "Your **{planName}** subscription for **{workspaceName}** is set to cancel. Everything stays available until **{accessEndsText}**, so you keep what you paid for.", "name": "Subscription cancel scheduled", + "notes": "After that date the workspace moves to the free plan. Changed your mind? You can keep the subscription anytime before then.\n\nIf this was unexpected, reply to this email and we'll sort it out.", + "preheader": "You can keep the subscription anytime before then.", "slug": "subscription-cancel-scheduled", "subject": "Your {planName} plan stays active until {accessEndsText}" }, "subscription-canceled": { - "body": "Hi,
Your {planName} subscription for {workspaceName} has been canceled. The workspace is now on the free plan.
Changed your mind? You can resubscribe anytime and pick up where you left off.
| Reactivate |
If this was unexpected, reply to this email and we'll sort it out.
", + "about": "billing for your Contentrain Studio workspace", + "cta_label": "Reactivate", + "cta_url": "{billingUrl}", + "heading": "Your subscription has been canceled", + "lead": "Your **{planName}** subscription for **{workspaceName}** has been canceled. The workspace is now on the free plan.", "name": "Subscription — Canceled", + "notes": "Changed your mind? You can resubscribe anytime and pick up where you left off.\n\nIf this was unexpected, reply to this email and we'll sort it out.", + "preheader": "You can resubscribe anytime.", "slug": "subscription-canceled", "subject": "Your Contentrain Studio subscription has been canceled" }, "trial-ending": { - "body": "Hi,
Your Contentrain Studio trial for {workspaceName} ends {trialEndsText}.
Once the trial ends, your {planName} subscription starts automatically at {planPrice}/month — no action required if that's what you want.
If you'd like to change your plan or cancel before it renews, manage your subscription below:
Questions? Reply to this email.
", + "about": "billing for your Contentrain Studio workspace", + "cta_label": "Manage billing", + "cta_url": "{billingUrl}", + "heading": "Your trial ends {trialEndsText}", + "lead": "Your Contentrain Studio trial for **{workspaceName}** ends **{trialEndsText}**.", "name": "Trial Ending Reminder", + "notes": "Once the trial ends, your {planName} subscription starts automatically at {planPrice}/month. No action is required if that's what you want.\n\nIf you'd like to change your plan or cancel before it renews, manage your subscription with the button above.\n\nQuestions? Reply to this email.", + "preheader": "Your plan starts automatically when the trial ends.", "slug": "trial-ending", "subject": "Your Contentrain Studio trial ends {trialEndsText}" }, "webhook-dlq-alert": { - "body": "Hi {recipientName},
A webhook in your {workspaceName} workspace has permanently failed after 5 delivery attempts.
| Webhook | {webhookName} |
| URL | {webhookUrl} |
| Event | {eventType} |
Please check the endpoint is reachable and responding with a 2xx status code.
", + "about": "webhook delivery in your Contentrain Studio workspace", + "cta_label": "Review webhook settings", + "cta_url": "{settingsUrl}", + "facts": "Webhook|**{webhookName}**\nURL|`{webhookUrl}`\nEvent|{eventType}", + "heading": "A webhook delivery failed permanently", + "lead": "Hi {recipientName}, a webhook in your **{workspaceName}** workspace has permanently failed after 5 delivery attempts.", "name": "Webhook Delivery Failed", + "notes": "Please check the endpoint is reachable and responding with a 2xx status code.", + "preheader": "Check that the endpoint is reachable.", "slug": "webhook-dlq-alert", "subject": "Webhook delivery failed: {webhookName} — {workspaceName}" }, "form-submitted": { - "body": "Hi,
A new submission arrived on the {modelName} form of {projectName} ({workspaceName}).
{summaryHtml}", + "about": "form submissions in your Contentrain Studio workspace", + "cta_label": "Review submissions", + "cta_url": "{submissionsUrl}", + "heading": "New {modelName} submission", + "lead": "A new submission arrived on the **{modelName}** form of **{projectName}** ({workspaceName}).", "name": "Form — New Submission", + "preheader": "Review the submission in Studio.", "slug": "form-submitted", "subject": "New {modelName} submission on {projectName}" }, "usage-warning": { + "about": "usage in your Contentrain Studio workspace", + "cta_label": "Open Billing", + "cta_url": "{billingUrl}", + "heading": "{percentage}% of {meterName} used", + "lead": "**{workspaceName}** has used **{used}** of the **{limit}** {meterName} included in the {planName} plan ({percentage}%).", "name": "Usage — 80% of a limit", + "notes": "{resetLine} If you expect to need more, you can change the plan in Billing.", + "preheader": "Open Billing to review the plan.", "slug": "usage-warning", - "subject": "{workspaceName}: {percentage}% of {meterName} used", - "body": "Hi,
{workspaceName} has used {used} of the {limit} {meterName} included in the {planName} plan ({percentage}%).
{resetLine} If you expect to need more, you can change the plan in Billing.
| Open Billing |
Hi,
{workspaceName} has used all {limit} {meterName} included in the {planName} plan.
{consequence}
{nextStep}
| Open Billing |
Hi,
{workspaceName} has used all {limit} {meterName} included in the {planName} plan. Overage is on, so nothing has stopped: usage past the limit is billed at {unitPrice} per unit on your next invoice.
{resetLine} You can turn overage off or change the plan in Billing.
| Open Billing |
Hi,
A new comment on {entryId} ({modelName}) of {projectName} ({workspaceName}) is waiting for your approval.
{authorName} wrote:
{excerptHtml}", + "about": "comments in your Contentrain Studio workspace", + "cta_label": "Review comments", + "cta_url": "{moderationUrl}", + "heading": "A new comment is waiting for approval", + "lead": "A new comment on **{entryId}** ({modelName}) of **{projectName}** ({workspaceName}) is waiting for your approval. **{authorName}** wrote:", "name": "Comment — Waiting for Approval", + "preheader": "Review the comment in Studio.", + "quote": "{excerpt}", "slug": "comment-pending", "subject": "New comment to approve on {projectName}" }, "comment-published": { - "body": "
Hi,
A new comment was published on {entryId} ({modelName}) of {projectName} ({workspaceName}).
{authorName} wrote:
{excerptHtml}", + "about": "comments in your Contentrain Studio workspace", + "cta_label": "Review comments", + "cta_url": "{moderationUrl}", + "heading": "A new comment was published", + "lead": "A new comment was published on **{entryId}** ({modelName}) of **{projectName}** ({workspaceName}). **{authorName}** wrote:", "name": "Comment — Published", + "preheader": "Review the comment in Studio.", + "quote": "{excerpt}", "slug": "comment-published", "subject": "New comment on {projectName}" + }, + "supabase-confirmation": { + "about": "your Contentrain Studio account", + "cta_label": "Confirm email", + "cta_url": "{url}", + "heading": "Confirm your email", + "lead": "Confirm your email address to activate your Contentrain Studio account.", + "name": "Supabase Auth — Confirm Email", + "notes": "If you didn't create this account, you can safely ignore this email.", + "preheader": "Confirm your email to activate your account.", + "slug": "supabase-confirmation", + "subject": "Confirm your Contentrain Studio account" + }, + "supabase-invite": { + "about": "your invitation to Contentrain Studio", + "cta_label": "Accept invitation", + "cta_url": "{url}", + "heading": "You're invited to Contentrain Studio", + "lead": "You've been invited to collaborate on Contentrain Studio. Accept the invitation to get started.", + "name": "Supabase Auth — Account Invitation", + "notes": "This link will expire in 24 hours. If you didn't expect this invitation, you can safely ignore this email.", + "preheader": "Accept your invitation to Contentrain Studio.", + "slug": "supabase-invite", + "subject": "You've been invited to Contentrain Studio" + }, + "supabase-magic-link": { + "about": "a sign-in request for Contentrain Studio", + "cta_label": "Sign in", + "cta_url": "{url}", + "heading": "Sign in to Contentrain Studio", + "lead": "Use the button below to sign in. No password needed.", + "name": "Supabase Auth — Magic Link Sign-in", + "notes": "This link will expire in 1 hour. If you didn't request this link, you can safely ignore this email.", + "preheader": "Your sign-in link for Contentrain Studio.", + "slug": "supabase-magic-link", + "subject": "Sign in to Contentrain Studio" } } diff --git a/.contentrain/content/system/error-messages/en.json b/.contentrain/content/system/error-messages/en.json index 5ce787ba..07934f06 100644 --- a/.contentrain/content/system/error-messages/en.json +++ b/.contentrain/content/system/error-messages/en.json @@ -325,7 +325,7 @@ "usage_alert.grace_cdn_bandwidth": "Your site is still being delivered: delivery continues up to {percentage}% of the limit, then stops until {date}. Change the plan to keep it running.", "usage_alert.next_overage": "To keep going now, allow overage or change the plan in Billing.", "usage_alert.next_upgrade": "To keep going now, change the plan in Billing.", - "usage_alert.resets_on": "The count resets on {date}.", + "usage_alert.resets_on": "The count resets on {date}.", "usage_alert.stopped_ai_messages": "Studio AI answers are paused until {date}. Members using their own AI key are not affected.", "usage_alert.stopped_api_messages": "Conversation API requests are refused until {date}.", "usage_alert.stopped_cdn_bandwidth": "Content and media delivery from the CDN has stopped until {date}. Visitors' requests are refused.", diff --git a/.contentrain/content/system/ui-strings/en.json b/.contentrain/content/system/ui-strings/en.json index 96e45d0d..d3d1f085 100644 --- a/.contentrain/content/system/ui-strings/en.json +++ b/.contentrain/content/system/ui-strings/en.json @@ -580,6 +580,17 @@ "editor.view_edit": "Edit", "editor.view_preview": "Preview", "editor.view_split": "Split", + "email.about_default": "your Contentrain Studio account", + "email.about_line": "This email is about {about}.", + "email.link_github": "GitHub", + "email.link_product": "Studio", + "email.link_website": "Website", + "email.logo_alt": "Contentrain Studio", + "email.privacy": "Privacy", + "email.product_line": "{product} is a Contentrain product.", + "email.product_name": "Studio", + "email.team": "The Contentrain team", + "email.thanks": "Thanks,", "error.402_description": "Your trial has expired or payment is required. Subscribe to a plan to continue using this workspace.", "error.402_title": "Payment Required", "error.403_description": "You don't have permission to access this page.", diff --git a/.contentrain/meta/email-templates/en.json b/.contentrain/meta/email-templates/en.json index 9bf4e7d3..08058ba8 100644 --- a/.contentrain/meta/email-templates/en.json +++ b/.contentrain/meta/email-templates/en.json @@ -68,5 +68,20 @@ "source": "import", "status": "draft", "updated_by": "contentrain-mcp" + }, + "supabase-confirmation": { + "source": "human", + "status": "published", + "updated_by": "contentrain-mcp" + }, + "supabase-invite": { + "source": "human", + "status": "published", + "updated_by": "contentrain-mcp" + }, + "supabase-magic-link": { + "source": "human", + "status": "published", + "updated_by": "contentrain-mcp" } } diff --git a/.contentrain/models/email-templates.json b/.contentrain/models/email-templates.json index b049d022..01030505 100644 --- a/.contentrain/models/email-templates.json +++ b/.contentrain/models/email-templates.json @@ -5,18 +5,50 @@ "domain": "system", "i18n": true, "title_field": "name", - "description": "Transactional email templates with subject and HTML body. Supports {variable} interpolation at runtime.", + "description": "Transactional email copy. Each entry fills the shared layout (server/utils/email-layout.ts): heading, lead, facts, quote, button, notes. Values are escaped at render time; no HTML.", "fields": { - "body": { - "description": "Email HTML body — supports {variable} interpolation", + "about": { + "description": "What the email is about, for the footer (\"This email is about …\"); plain text", + "type": "string" + }, + "cta_label": { + "description": "Button label", + "type": "string" + }, + "cta_url": { + "description": "Button URL, a single {param} that holds an absolute URL", + "type": "string" + }, + "facts": { + "description": "Optional key/value rows, one per line as \"Label|value\" — values support {param}, **bold**, `code`", + "type": "text" + }, + "heading": { + "description": "Email heading (h1) — supports {param} interpolation", "required": true, - "type": "richtext" + "type": "string" + }, + "lead": { + "description": "Lead paragraph — {param} interpolation, **bold**, `code`, [label]({urlParam})", + "type": "text" }, "name": { "description": "Human-readable template name for admin reference", "required": true, "type": "string" }, + "notes": { + "description": "Secondary paragraphs below the rule, separated by a blank line — same markup as lead", + "type": "text" + }, + "preheader": { + "description": "Hidden inbox preview text — supports {param} interpolation", + "type": "string" + }, + "quote": { + "description": "Optional quoted user text, usually a single {param}; rendered as plain escaped text", + "type": "string" + }, "slug": { "description": "Template identifier used in code (e.g. invite-added)", "required": true, @@ -24,7 +56,7 @@ "unique": true }, "subject": { - "description": "Email subject line — supports {variable} interpolation", + "description": "Email subject line — supports {param} interpolation (plain text)", "required": true, "type": "string" } diff --git a/.env.example b/.env.example index 800ae095..e25d0f72 100644 --- a/.env.example +++ b/.env.example @@ -164,6 +164,12 @@ NUXT_RESEND_API_KEY=re_your-resend-api-key RESEND_API_KEY=re_your-resend-api-key NUXT_EMAIL_SENDER_ADDRESS=noreply@example.com NUXT_EMAIL_SENDER_NAME=Contentrain Studio +# Where a reply to any Studio email goes (the copy says "reply to this email"). Default: support@contentrain.io. +# NUXT_EMAIL_REPLY_TO=support@example.com +# Footer links of every email (defaults point at contentrain.io). +# NUXT_EMAIL_LINKS_WEBSITE_URL=https://contentrain.io +# NUXT_EMAIL_LINKS_GITHUB_URL=https://github.com/Contentrain +# NUXT_EMAIL_LINKS_PRIVACY_URL=https://contentrain.io/legal/privacy-policy # ----------------------------------------------------------------------------- # Billing — Polar (default provider) diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 0836c231..7fb10933 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -83,6 +83,7 @@ Managed + postgres pair (plain PostgreSQL, no Supabase): - Set `NUXT_RESEND_API_KEY` (optional on the supabase pair; **required** on the managed pair — magic link + invites) - Set `NUXT_EMAIL_SENDER_ADDRESS` - Set `NUXT_EMAIL_SENDER_NAME` +- Optional: `NUXT_EMAIL_REPLY_TO` (default `support@contentrain.io`) and `NUXT_EMAIL_LINKS_*` (footer links). Emails load their logo from `
+
|