From cb5600ad454827ac427af01b79d5914627bf956c Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Tue, 7 Oct 2025 11:47:59 +0100 Subject: [PATCH] feat: doc templating & cleanup of routes (#4411) * feat: clean up route structure * feat: install html-pdf-node-ts * fea * feat: use @ceereals/vue-pdf (react-pdf) * feat: remove pdf * feat: hide cc * feat: shared template * feat: payment statement document & redirect for emails * feat: layout tweaks * fix: lint issues * fix: robots.txt * feat: remove letterhead * Delete .claude/settings.local.json Signed-off-by: Calum H. --------- Signed-off-by: Calum H. --- .gitignore | 1 + apps/frontend/nuxt.config.ts | 20 +- apps/frontend/src/emails/index.ts | 37 --- .../src/emails/shared/StyledEmail.vue | 250 ------------------ apps/frontend/src/pages/admin/docs.vue | 131 +++++++++ apps/frontend/src/pages/admin/emails.vue | 12 +- apps/frontend/src/public/robots.txt | 2 +- .../_internal/templates/doc/[template].ts | 28 ++ .../templates}/email/[template].ts | 2 +- .../docs/finance/PaymentStatement.vue | 107 ++++++++ apps/frontend/src/templates/docs/index.ts | 6 + .../src/templates/docs/shared/StyledDoc.vue | 54 ++++ .../account/AuthenticationMethodAdded.vue | 2 +- .../account/AuthenticationMethodRemoved.vue | 2 +- .../emails}/account/EmailChanged.vue | 2 +- .../emails}/account/LoginNewDevice.vue | 2 +- .../emails}/account/PATCreated.vue | 2 +- .../emails}/account/PasswordChanged.vue | 2 +- .../emails}/account/PasswordRemoved.vue | 2 +- .../emails}/account/PaymentFailed.vue | 2 +- .../emails}/account/PayoutAvailable.vue | 2 +- .../emails}/account/ResetPassword.vue | 2 +- .../emails}/account/SubscriptionTaxChange.vue | 2 +- .../emails}/account/TwoFactorAdded.vue | 2 +- .../emails}/account/TwoFactorRemoved.vue | 2 +- .../emails}/account/VerifyEmail.vue | 2 +- apps/frontend/src/templates/emails/index.ts | 36 +++ .../ModerationThreadMessageReceived.vue | 2 +- .../moderation/ReportStatusUpdated.vue | 2 +- .../emails}/moderation/ReportSubmitted.vue | 2 +- .../organization/OrganizationInvited.vue | 2 +- .../emails}/project/ProjectInvited.vue | 2 +- .../emails}/project/ProjectStatusApproved.vue | 2 +- .../project/ProjectStatusUpdatedNeutral.vue | 2 +- .../emails}/project/ProjectTransferred.vue | 2 +- .../templates/emails/shared/StyledEmail.vue | 161 +++++++++++ .../src/templates/shared/StyledTemplate.vue | 92 +++++++ pnpm-lock.yaml | 23 +- 38 files changed, 681 insertions(+), 325 deletions(-) delete mode 100644 apps/frontend/src/emails/index.ts delete mode 100644 apps/frontend/src/emails/shared/StyledEmail.vue create mode 100644 apps/frontend/src/pages/admin/docs.vue create mode 100644 apps/frontend/src/server/routes/_internal/templates/doc/[template].ts rename apps/frontend/src/server/routes/{ => _internal/templates}/email/[template].ts (94%) create mode 100644 apps/frontend/src/templates/docs/finance/PaymentStatement.vue create mode 100644 apps/frontend/src/templates/docs/index.ts create mode 100644 apps/frontend/src/templates/docs/shared/StyledDoc.vue rename apps/frontend/src/{emails/templates => templates/emails}/account/AuthenticationMethodAdded.vue (93%) rename apps/frontend/src/{emails/templates => templates/emails}/account/AuthenticationMethodRemoved.vue (93%) rename apps/frontend/src/{emails/templates => templates/emails}/account/EmailChanged.vue (93%) rename apps/frontend/src/{emails/templates => templates/emails}/account/LoginNewDevice.vue (97%) rename apps/frontend/src/{emails/templates => templates/emails}/account/PATCreated.vue (93%) rename apps/frontend/src/{emails/templates => templates/emails}/account/PasswordChanged.vue (93%) rename apps/frontend/src/{emails/templates => templates/emails}/account/PasswordRemoved.vue (94%) rename apps/frontend/src/{emails/templates => templates/emails}/account/PaymentFailed.vue (94%) rename apps/frontend/src/{emails/templates => templates/emails}/account/PayoutAvailable.vue (95%) rename apps/frontend/src/{emails/templates => templates/emails}/account/ResetPassword.vue (94%) rename apps/frontend/src/{emails/templates => templates/emails}/account/SubscriptionTaxChange.vue (96%) rename apps/frontend/src/{emails/templates => templates/emails}/account/TwoFactorAdded.vue (94%) rename apps/frontend/src/{emails/templates => templates/emails}/account/TwoFactorRemoved.vue (94%) rename apps/frontend/src/{emails/templates => templates/emails}/account/VerifyEmail.vue (94%) create mode 100644 apps/frontend/src/templates/emails/index.ts rename apps/frontend/src/{emails/templates => templates/emails}/moderation/ModerationThreadMessageReceived.vue (96%) rename apps/frontend/src/{emails/templates => templates/emails}/moderation/ReportStatusUpdated.vue (95%) rename apps/frontend/src/{emails/templates => templates/emails}/moderation/ReportSubmitted.vue (96%) rename apps/frontend/src/{emails/templates => templates/emails}/organization/OrganizationInvited.vue (97%) rename apps/frontend/src/{emails/templates => templates/emails}/project/ProjectInvited.vue (97%) rename apps/frontend/src/{emails/templates => templates/emails}/project/ProjectStatusApproved.vue (96%) rename apps/frontend/src/{emails/templates => templates/emails}/project/ProjectStatusUpdatedNeutral.vue (97%) rename apps/frontend/src/{emails/templates => templates/emails}/project/ProjectTransferred.vue (96%) create mode 100644 apps/frontend/src/templates/emails/shared/StyledEmail.vue create mode 100644 apps/frontend/src/templates/shared/StyledTemplate.vue diff --git a/.gitignore b/.gitignore index 3031aa17..b02d5009 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ generated app-playground-data/* .astro +.claude diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index e2d9bfe1..f1ed046d 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -114,13 +114,19 @@ export default defineNuxtConfig({ hooks: { async 'nitro:config'(nitroConfig) { const emailTemplates = Object.keys( - await import('./src/emails/index.ts').then((m) => m.default), + await import('./src/templates/emails/index.ts').then((m) => m.default), + ) + const docTemplates = Object.keys( + await import('./src/templates/docs/index.ts').then((m) => m.default), ) nitroConfig.prerender = nitroConfig.prerender || {} nitroConfig.prerender.routes = nitroConfig.prerender.routes || [] for (const template of emailTemplates) { - nitroConfig.prerender.routes.push(`/email/${template}`) + nitroConfig.prerender.routes.push(`/_internal/templates/email/${template}`) + } + for (const template of docTemplates) { + nitroConfig.prerender.routes.push(`/_internal/templates/doc/${template}`) } }, async 'build:before'() { @@ -470,6 +476,16 @@ export default defineNuxtConfig({ }, }, '/email/**': { + redirect: '/_internal/templates/email/**', + }, + '/_internal/templates/email/**': { + prerender: true, + headers: { + 'Content-Type': 'text/html', + 'Cache-Control': 'public, max-age=3600', + }, + }, + '/_internal/templates/doc/**': { prerender: true, headers: { 'Content-Type': 'text/html', diff --git a/apps/frontend/src/emails/index.ts b/apps/frontend/src/emails/index.ts deleted file mode 100644 index adb35548..00000000 --- a/apps/frontend/src/emails/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Component } from 'vue' - -export default { - // Account - 'auth-method-added': () => import('./templates/account/AuthenticationMethodAdded.vue'), - 'auth-method-removed': () => import('./templates/account/AuthenticationMethodRemoved.vue'), - 'email-changed': () => import('./templates/account/EmailChanged.vue'), - 'password-changed': () => import('./templates/account/PasswordChanged.vue'), - 'password-removed': () => import('./templates/account/PasswordRemoved.vue'), - 'payment-failed': () => import('./templates/account/PaymentFailed.vue'), - 'reset-password': () => import('./templates/account/ResetPassword.vue'), - 'two-factor-added': () => import('./templates/account/TwoFactorAdded.vue'), - 'two-factor-removed': () => import('./templates/account/TwoFactorRemoved.vue'), - 'verify-email': () => import('./templates/account/VerifyEmail.vue'), - 'login-new-device': () => import('./templates/account/LoginNewDevice.vue'), - 'payout-available': () => import('./templates/account/PayoutAvailable.vue'), - 'personal-access-token-created': () => import('./templates/account/PATCreated.vue'), - - // Subscriptions - 'subscription-tax-change': () => import('./templates/account/SubscriptionTaxChange.vue'), - - // Moderation - 'report-submitted': () => import('./templates/moderation/ReportSubmitted.vue'), - 'report-status-updated': () => import('./templates/moderation/ReportStatusUpdated.vue'), - 'moderation-thread-message-received': () => - import('./templates/moderation/ModerationThreadMessageReceived.vue'), - - // Project - 'project-status-updated-neutral': () => - import('./templates/project/ProjectStatusUpdatedNeutral.vue'), - 'project-status-approved': () => import('./templates/project/ProjectStatusApproved.vue'), - 'project-invited': () => import('./templates/project/ProjectInvited.vue'), - 'project-transferred': () => import('./templates/project/ProjectTransferred.vue'), - - // Organization - 'organization-invited': () => import('./templates/organization/OrganizationInvited.vue'), -} as Record Promise<{ default: Component }>> diff --git a/apps/frontend/src/emails/shared/StyledEmail.vue b/apps/frontend/src/emails/shared/StyledEmail.vue deleted file mode 100644 index 10cbc3e0..00000000 --- a/apps/frontend/src/emails/shared/StyledEmail.vue +++ /dev/null @@ -1,250 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/admin/docs.vue b/apps/frontend/src/pages/admin/docs.vue new file mode 100644 index 00000000..cfeb36cf --- /dev/null +++ b/apps/frontend/src/pages/admin/docs.vue @@ -0,0 +1,131 @@ + + + diff --git a/apps/frontend/src/pages/admin/emails.vue b/apps/frontend/src/pages/admin/emails.vue index c7b86399..a54386e7 100644 --- a/apps/frontend/src/pages/admin/emails.vue +++ b/apps/frontend/src/pages/admin/emails.vue @@ -3,7 +3,7 @@ import { CopyIcon, LibraryIcon, PlayIcon, SearchIcon } from '@modrinth/assets' import { ButtonStyled, Card } from '@modrinth/ui' import { computed, onMounted, ref } from 'vue' -import emails from '@/emails' +import emails from '~/templates/emails' const allTemplates = Object.keys(emails).sort() const query = ref('') @@ -20,7 +20,7 @@ function openAll() { } function copy(id: string) { - navigator.clipboard?.writeText(`/email/${id}`).catch(() => {}) + navigator.clipboard?.writeText(`/_internal/templates/email/${id}`).catch(() => {}) } function openPreview(id: string, offset = 0) { @@ -29,7 +29,7 @@ function openPreview(id: string, offset = 0) { const left = window.screenX + (window.outerWidth - width) / 2 + ((offset * 28) % 320) const top = window.screenY + (window.outerHeight - height) / 2 + ((offset * 28) % 320) window.open( - `/email/${id}`, + `/_internal/templates/email/${id}`, `email-${id}`, `popup=yes,width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,menubar=no,toolbar=no,location=no,status=no`, ) @@ -94,7 +94,9 @@ onMounted(() => {
{{ id }}
-
/email/{{ id }}
+
+ /_internal/templates/email/{{ id }} +
@@ -121,7 +123,7 @@ onMounted(() => { >src/emails/index.ts. Popouts render via /email/[template]/_internal/templates/email/[template].

diff --git a/apps/frontend/src/public/robots.txt b/apps/frontend/src/public/robots.txt index 9c46f4a3..dc2e53a7 100644 --- a/apps/frontend/src/public/robots.txt +++ b/apps/frontend/src/public/robots.txt @@ -1,2 +1,2 @@ User-agent: * -Disallow: /email/ +Disallow: /_internal/ diff --git a/apps/frontend/src/server/routes/_internal/templates/doc/[template].ts b/apps/frontend/src/server/routes/_internal/templates/doc/[template].ts new file mode 100644 index 00000000..e3aa6ff9 --- /dev/null +++ b/apps/frontend/src/server/routes/_internal/templates/doc/[template].ts @@ -0,0 +1,28 @@ +import { render } from '@vue-email/render' +import type { Component } from 'vue' + +import docs from '~/templates/docs' + +export default defineEventHandler(async (event) => { + const template = event.context.params?.template as string + try { + const component = (await docs[template]()).default as Component | undefined + + if (!component) { + throw createError({ + statusCode: 404, + message: 'Document template not found', + }) + } + + const html = await render(component, {}) + + return html + } catch (error) { + console.error(`Error rendering document template ${template}:`, error) + throw createError({ + statusCode: 500, + message: 'Failed to render document template', + }) + } +}) diff --git a/apps/frontend/src/server/routes/email/[template].ts b/apps/frontend/src/server/routes/_internal/templates/email/[template].ts similarity index 94% rename from apps/frontend/src/server/routes/email/[template].ts rename to apps/frontend/src/server/routes/_internal/templates/email/[template].ts index dce301d0..0614de9f 100644 --- a/apps/frontend/src/server/routes/email/[template].ts +++ b/apps/frontend/src/server/routes/_internal/templates/email/[template].ts @@ -1,7 +1,7 @@ import { render } from '@vue-email/render' import type { Component } from 'vue' -import emails from '~/emails' +import emails from '~/templates/emails' export default defineEventHandler(async (event) => { const template = event.context.params?.template as string diff --git a/apps/frontend/src/templates/docs/finance/PaymentStatement.vue b/apps/frontend/src/templates/docs/finance/PaymentStatement.vue new file mode 100644 index 00000000..dea235ba --- /dev/null +++ b/apps/frontend/src/templates/docs/finance/PaymentStatement.vue @@ -0,0 +1,107 @@ + + + diff --git a/apps/frontend/src/templates/docs/index.ts b/apps/frontend/src/templates/docs/index.ts new file mode 100644 index 00000000..c42d7bd3 --- /dev/null +++ b/apps/frontend/src/templates/docs/index.ts @@ -0,0 +1,6 @@ +import type { Component } from 'vue' + +export default { + // Finance + 'payment-statement': () => import('./finance/PaymentStatement.vue'), +} as Record Promise<{ default: Component }>> diff --git a/apps/frontend/src/templates/docs/shared/StyledDoc.vue b/apps/frontend/src/templates/docs/shared/StyledDoc.vue new file mode 100644 index 00000000..049e88dd --- /dev/null +++ b/apps/frontend/src/templates/docs/shared/StyledDoc.vue @@ -0,0 +1,54 @@ + + + diff --git a/apps/frontend/src/emails/templates/account/AuthenticationMethodAdded.vue b/apps/frontend/src/templates/emails/account/AuthenticationMethodAdded.vue similarity index 93% rename from apps/frontend/src/emails/templates/account/AuthenticationMethodAdded.vue rename to apps/frontend/src/templates/emails/account/AuthenticationMethodAdded.vue index a2f33741..278ea0a3 100644 --- a/apps/frontend/src/emails/templates/account/AuthenticationMethodAdded.vue +++ b/apps/frontend/src/templates/emails/account/AuthenticationMethodAdded.vue @@ -1,7 +1,7 @@