diff --git a/.gitignore b/.gitignore index 3031aa173b..b02d500994 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 e2d9bfe113..f1ed046de9 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 adb3554880..0000000000 --- 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 10cbc3e0eb..0000000000 --- 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 0000000000..cfeb36cf97 --- /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 c7b863993c..a54386e709 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 9c46f4a38c..dc2e53a71f 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 0000000000..e3aa6ff95c --- /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 dce301d0c1..0614de9f14 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 0000000000..dea235ba6f --- /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 0000000000..c42d7bd32d --- /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 0000000000..049e88dde6 --- /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 a2f3374166..278ea0a348 100644 --- a/apps/frontend/src/emails/templates/account/AuthenticationMethodAdded.vue +++ b/apps/frontend/src/templates/emails/account/AuthenticationMethodAdded.vue @@ -1,7 +1,7 @@