From 5603a9e59da2ae770edcc822ac08a5e3df02ad82 Mon Sep 17 00:00:00 2001 From: Christopher Ryan <37274436+GMCfourX4@users.noreply.github.com> Date: Fri, 11 Sep 2026 06:44:05 -0400 Subject: [PATCH] fix: preserve base path in email asset URLs (#3327) --- .../template-access-auth-2fa.tsx | 7 ++----- .../template-branding-logo.tsx | 3 ++- .../template-document-completed.tsx | 13 +++++++------ .../template-document-image.tsx | 7 ++----- .../template-document-pending.tsx | 11 ++++++----- .../template-document-recipient-signed.tsx | 7 ++----- .../template-document-self-signed.tsx | 15 ++++++++------- .../template-components/template-image.tsx | 7 ++----- packages/email/templates/admin-user-created.tsx | 7 ++----- packages/email/utils/asset-url.ts | 17 +++++++++++++++++ 10 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 packages/email/utils/asset-url.ts diff --git a/packages/email/template-components/template-access-auth-2fa.tsx b/packages/email/template-components/template-access-auth-2fa.tsx index 7873a1735..a7918ad4b 100644 --- a/packages/email/template-components/template-access-auth-2fa.tsx +++ b/packages/email/template-components/template-access-auth-2fa.tsx @@ -1,6 +1,7 @@ import { Plural, Trans } from '@lingui/react/macro'; import { Heading, Img, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; export type TemplateAccessAuth2FAProps = { documentTitle: string; @@ -18,13 +19,9 @@ export const TemplateAccessAuth2FA = ({ expiresInMinutes, assetBaseUrl = 'http://localhost:3002', }: TemplateAccessAuth2FAProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return (
- Document + Document
diff --git a/packages/email/template-components/template-branding-logo.tsx b/packages/email/template-components/template-branding-logo.tsx index c7e99684e..6b560aed2 100644 --- a/packages/email/template-components/template-branding-logo.tsx +++ b/packages/email/template-components/template-branding-logo.tsx @@ -1,5 +1,6 @@ import { Img, Link } from '../components'; import { useBranding } from '../providers/branding'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { getSafeBrandingUrl } from '../utils/branding-url'; export type TemplateBrandingLogoProps = { @@ -20,7 +21,7 @@ export const TemplateBrandingLogo = ({ assetBaseUrl, className = 'mb-4 h-6' }: T const hasCustomBrandingLogo = branding.brandingEnabled && Boolean(branding.brandingLogo); if (!hasCustomBrandingLogo) { - const documensoLogoUrl = new URL('/static/logo.png', assetBaseUrl).toString(); + const documensoLogoUrl = getEmailAssetUrl(assetBaseUrl, 'static/logo.png'); return Documenso Logo; } diff --git a/packages/email/template-components/template-document-completed.tsx b/packages/email/template-components/template-document-completed.tsx index ec453f315..20b7000e1 100644 --- a/packages/email/template-components/template-document-completed.tsx +++ b/packages/email/template-components/template-document-completed.tsx @@ -1,6 +1,7 @@ import { Trans } from '@lingui/react/macro'; import { Button, Column, Img, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { TemplateDocumentImage } from './template-document-image'; export interface TemplateDocumentCompletedProps { @@ -16,10 +17,6 @@ export const TemplateDocumentCompleted = ({ assetBaseUrl, customBody, }: TemplateDocumentCompletedProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return ( <> @@ -29,7 +26,7 @@ export const TemplateDocumentCompleted = ({ @@ -51,7 +48,11 @@ export const TemplateDocumentCompleted = ({ className="rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" href={downloadLink} > - + Download
diff --git a/packages/email/template-components/template-document-image.tsx b/packages/email/template-components/template-document-image.tsx index ed45512a5..88c06282c 100644 --- a/packages/email/template-components/template-document-image.tsx +++ b/packages/email/template-components/template-document-image.tsx @@ -1,4 +1,5 @@ import { Column, Img, Row, Section } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; export interface TemplateDocumentImageProps { assetBaseUrl: string; @@ -6,17 +7,13 @@ export interface TemplateDocumentImageProps { } export const TemplateDocumentImage = ({ assetBaseUrl, className }: TemplateDocumentImageProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return (
- Documenso + Documenso diff --git a/packages/email/template-components/template-document-pending.tsx b/packages/email/template-components/template-document-pending.tsx index c44ae5c81..5cc435e28 100644 --- a/packages/email/template-components/template-document-pending.tsx +++ b/packages/email/template-components/template-document-pending.tsx @@ -1,6 +1,7 @@ import { Trans } from '@lingui/react/macro'; import { Column, Img, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { TemplateDocumentImage } from './template-document-image'; export interface TemplateDocumentPendingProps { @@ -9,10 +10,6 @@ export interface TemplateDocumentPendingProps { } export const TemplateDocumentPending = ({ documentName, assetBaseUrl }: TemplateDocumentPendingProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return ( <> @@ -21,7 +18,11 @@ export const TemplateDocumentPending = ({ documentName, assetBaseUrl }: Template
- + Waiting for others diff --git a/packages/email/template-components/template-document-recipient-signed.tsx b/packages/email/template-components/template-document-recipient-signed.tsx index 42ebe0e10..9077091fb 100644 --- a/packages/email/template-components/template-document-recipient-signed.tsx +++ b/packages/email/template-components/template-document-recipient-signed.tsx @@ -1,6 +1,7 @@ import { Trans } from '@lingui/react/macro'; import { Column, Img, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { TemplateDocumentImage } from './template-document-image'; export interface TemplateDocumentRecipientSignedProps { @@ -16,10 +17,6 @@ export const TemplateDocumentRecipientSigned = ({ recipientEmail, assetBaseUrl, }: TemplateDocumentRecipientSignedProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - const recipientReference = recipientName || recipientEmail; return ( @@ -31,7 +28,7 @@ export const TemplateDocumentRecipientSigned = ({ diff --git a/packages/email/template-components/template-document-self-signed.tsx b/packages/email/template-components/template-document-self-signed.tsx index 852843577..729a3b156 100644 --- a/packages/email/template-components/template-document-self-signed.tsx +++ b/packages/email/template-components/template-document-self-signed.tsx @@ -2,6 +2,7 @@ import { env } from '@documenso/lib/utils/env'; import { Trans } from '@lingui/react/macro'; import { Button, Column, Img, Link, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { TemplateDocumentImage } from './template-document-image'; export interface TemplateDocumentSelfSignedProps { @@ -14,10 +15,6 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ const signUpUrl = `${NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`; - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return ( <> @@ -27,7 +24,7 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ @@ -56,7 +53,7 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ className="mr-4 rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" > @@ -67,7 +64,11 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ className="rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" href="https://documenso.com/pricing" > - + View plans
diff --git a/packages/email/template-components/template-image.tsx b/packages/email/template-components/template-image.tsx index b16bcfe60..198c55aec 100644 --- a/packages/email/template-components/template-image.tsx +++ b/packages/email/template-components/template-image.tsx @@ -1,4 +1,5 @@ import { Img } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; export interface TemplateImageProps { assetBaseUrl: string; @@ -7,11 +8,7 @@ export interface TemplateImageProps { } export const TemplateImage = ({ assetBaseUrl, className, staticAsset }: TemplateImageProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - - return ; + return ; }; export default TemplateImage; diff --git a/packages/email/templates/admin-user-created.tsx b/packages/email/templates/admin-user-created.tsx index ccc92020b..000b4d3cb 100644 --- a/packages/email/templates/admin-user-created.tsx +++ b/packages/email/templates/admin-user-created.tsx @@ -5,6 +5,7 @@ import { Body, Container, Head, Html, Img, Preview, Section } from '../component import type { TemplateAdminUserCreatedProps } from '../template-components/template-admin-user-created'; import { TemplateAdminUserCreated } from '../template-components/template-admin-user-created'; import { TemplateFooter } from '../template-components/template-footer'; +import { getEmailAssetUrl } from '../utils/asset-url'; export const AdminUserCreatedTemplate = ({ resetPasswordLink, @@ -14,10 +15,6 @@ export const AdminUserCreatedTemplate = ({ const previewText = msg`Set your password for Documenso`; - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return ( @@ -27,7 +24,7 @@ export const AdminUserCreatedTemplate = ({
- Documenso Logo + Documenso Logo
diff --git a/packages/email/utils/asset-url.ts b/packages/email/utils/asset-url.ts new file mode 100644 index 000000000..f6143de19 --- /dev/null +++ b/packages/email/utils/asset-url.ts @@ -0,0 +1,17 @@ +/** + * Resolve a static email asset path against the asset base URL. + * + * The base is normalised to end with a trailing slash and the path is + * normalised to have no leading slash, so a sub-path in the base URL + * (e.g. "/ESign") is preserved. Passing a root-absolute path straight to + * `new URL()` would otherwise replace the base pathname entirely. + * + * `getEmailAssetUrl('https://host/ESign', 'static/logo.png')` -> `https://host/ESign/static/logo.png` + * `getEmailAssetUrl('https://host/ESign/', '/static/logo.png')` -> `https://host/ESign/static/logo.png` + */ +export const getEmailAssetUrl = (assetBaseUrl: string, path: string): string => { + const base = assetBaseUrl.endsWith('/') ? assetBaseUrl : `${assetBaseUrl}/`; + const relativePath = path.startsWith('/') ? path.slice(1) : path; + + return new URL(relativePath, base).toString(); +};