diff --git a/README.md b/README.md
index 8ba8caf..ea61286 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
     * Email addresses can optionally be used instead of usernames
 * 🔑 Password resets: When user's forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email.
 * Notifications: Get notified when someone creates an account, or an invite expires.
-* 📣 Announcements: Bulk email you users with announcements about your server.
+* 📣 Announcements: Bulk email your users with announcements about your server.
 * Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
     * Enables the usage of jfa-go by multiple people
 * 🌓 Customizable look
diff --git a/api.go b/api.go
index 5cdf5b7..859137c 100644
--- a/api.go
+++ b/api.go
@@ -1375,87 +1375,9 @@ func (app *appContext) SetEmailState(gc *gin.Context) {
 	respondBool(200, true, gc)
 }
 
-// @Summary Render and return an email for testing purposes.
+// @Summary Returns the custom email (generating it if not set) and list of used variables in it.
 // @Produce json
-// @Param customEmail body customEmail true "Content = email (in markdown)."
-// @Success 200 {object} Email
-// @Failure 400 {object} boolResponse
-// @Failure 500 {object} boolResponse
-// @Router /config/emails/{id}/test [post]
-// @tags Configuration
-func (app *appContext) GetTestEmail(gc *gin.Context) {
-	var req customEmail
-	gc.BindJSON(&req)
-	if req.Content == "" {
-		app.debug.Println("Test failed: Content was empty")
-		respondBool(400, false, gc)
-		return
-	}
-	id := gc.Param("id")
-	var msg *Email
-	var err error
-	var cache customEmail
-	var restore func(cache customEmail)
-	username := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("username")
-	emailAddress := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("emailAddress")
-	if id == "UserCreated" {
-		cache = app.storage.customEmails.UserCreated
-		restore = func(cache customEmail) { app.storage.customEmails.UserCreated = cache }
-		app.storage.customEmails.UserCreated.Content = req.Content
-		app.storage.customEmails.UserCreated.Enabled = true
-		msg, err = app.email.constructCreated("xxxxxx", username, emailAddress, Invite{}, app, false)
-	} else if id == "InviteExpiry" {
-		cache = app.storage.customEmails.InviteExpiry
-		restore = func(cache customEmail) { app.storage.customEmails.InviteExpiry = cache }
-		app.storage.customEmails.InviteExpiry.Content = req.Content
-		app.storage.customEmails.InviteExpiry.Enabled = true
-		msg, err = app.email.constructExpiry("xxxxxx", Invite{}, app, false)
-	} else if id == "PasswordReset" {
-		cache = app.storage.customEmails.PasswordReset
-		restore = func(cache customEmail) { app.storage.customEmails.PasswordReset = cache }
-		app.storage.customEmails.PasswordReset.Content = req.Content
-		app.storage.customEmails.PasswordReset.Enabled = true
-		msg, err = app.email.constructReset(PasswordReset{Pin: "12-34-56", Username: username}, app, false)
-	} else if id == "UserDeleted" {
-		cache = app.storage.customEmails.UserDeleted
-		restore = func(cache customEmail) { app.storage.customEmails.UserDeleted = cache }
-		app.storage.customEmails.UserDeleted.Content = req.Content
-		app.storage.customEmails.UserDeleted.Enabled = true
-		msg, err = app.email.constructDeleted(app.storage.lang.Email[app.storage.lang.chosenEmailLang].UserDeleted.get("reason"), app, false)
-	} else if id == "InviteEmail" {
-		cache = app.storage.customEmails.InviteEmail
-		restore = func(cache customEmail) { app.storage.customEmails.InviteEmail = cache }
-		app.storage.customEmails.InviteEmail.Content = req.Content
-		app.storage.customEmails.InviteEmail.Enabled = true
-		msg, err = app.email.constructInvite("xxxxxx", Invite{}, app, false)
-	} else if id == "WelcomeEmail" {
-		cache = app.storage.customEmails.WelcomeEmail
-		restore = func(cache customEmail) { app.storage.customEmails.WelcomeEmail = cache }
-		app.storage.customEmails.WelcomeEmail.Content = req.Content
-		app.storage.customEmails.WelcomeEmail.Enabled = true
-		msg, err = app.email.constructWelcome(username, app, false)
-	} else if id == "EmailConfirmation" {
-		cache = app.storage.customEmails.EmailConfirmation
-		restore = func(cache customEmail) { app.storage.customEmails.EmailConfirmation = cache }
-		app.storage.customEmails.EmailConfirmation.Content = req.Content
-		app.storage.customEmails.EmailConfirmation.Enabled = true
-		msg, err = app.email.constructConfirmation("xxxxxx", username, "xxxxxx", app, false)
-	} else {
-		respondBool(400, false, gc)
-		return
-	}
-	restore(cache)
-	if err != nil {
-		respondBool(500, false, gc)
-		app.err.Printf("Failed to construct test email: %s", err)
-		return
-	}
-	gc.JSON(200, msg)
-}
-
-// @Summary Returns the boilerplate email and list of used variables in it.
-// @Produce json
-// @Success 200 {object} customEmail
+// @Success 200 {object} customEmailDTO
 // @Failure 400 {object} boolResponse
 // @Failure 500 {object} boolResponse
 // @Router /config/emails/{id} [get]
@@ -1466,8 +1388,11 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 	var err error
 	var msg *Email
 	var variables []string
+	var values map[string]interface{}
 	var writeVars func(variables []string)
 	newEmail := false
+	username := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("username")
+	emailAddress := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("emailAddress")
 	if id == "UserCreated" {
 		content = app.storage.customEmails.UserCreated.Content
 		if content == "" {
@@ -1478,6 +1403,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 			variables = app.storage.customEmails.UserCreated.Variables
 		}
 		writeVars = func(variables []string) { app.storage.customEmails.UserCreated.Variables = variables }
+		values = app.email.createdValues("xxxxxx", username, emailAddress, Invite{}, app, false)
 		// app.storage.customEmails.UserCreated = content
 	} else if id == "InviteExpiry" {
 		content = app.storage.customEmails.InviteExpiry.Content
@@ -1489,6 +1415,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 			variables = app.storage.customEmails.InviteExpiry.Variables
 		}
 		writeVars = func(variables []string) { app.storage.customEmails.InviteExpiry.Variables = variables }
+		values = app.email.expiryValues("xxxxxx", Invite{}, app, false)
 		// app.storage.customEmails.InviteExpiry = content
 	} else if id == "PasswordReset" {
 		content = app.storage.customEmails.PasswordReset.Content
@@ -1500,6 +1427,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 			variables = app.storage.customEmails.PasswordReset.Variables
 		}
 		writeVars = func(variables []string) { app.storage.customEmails.PasswordReset.Variables = variables }
+		values = app.email.resetValues(PasswordReset{Pin: "12-34-56", Username: username}, app, false)
 		// app.storage.customEmails.PasswordReset = content
 	} else if id == "UserDeleted" {
 		content = app.storage.customEmails.UserDeleted.Content
@@ -1511,6 +1439,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 			variables = app.storage.customEmails.UserDeleted.Variables
 		}
 		writeVars = func(variables []string) { app.storage.customEmails.UserDeleted.Variables = variables }
+		values = app.email.deletedValues(app.storage.lang.Email[app.storage.lang.chosenEmailLang].UserDeleted.get("reason"), app, false)
 		// app.storage.customEmails.UserDeleted = content
 	} else if id == "InviteEmail" {
 		content = app.storage.customEmails.InviteEmail.Content
@@ -1522,6 +1451,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 			variables = app.storage.customEmails.InviteEmail.Variables
 		}
 		writeVars = func(variables []string) { app.storage.customEmails.InviteEmail.Variables = variables }
+		values = app.email.inviteValues("xxxxxx", Invite{}, app, false)
 		// app.storage.customEmails.InviteEmail = content
 	} else if id == "WelcomeEmail" {
 		content = app.storage.customEmails.WelcomeEmail.Content
@@ -1534,6 +1464,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 		}
 		writeVars = func(variables []string) { app.storage.customEmails.WelcomeEmail.Variables = variables }
 		// app.storage.customEmails.WelcomeEmail = content
+		values = app.email.welcomeValues(username, app, false)
 	} else if id == "EmailConfirmation" {
 		content = app.storage.customEmails.EmailConfirmation.Content
 		if content == "" {
@@ -1544,6 +1475,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 			variables = app.storage.customEmails.EmailConfirmation.Variables
 		}
 		writeVars = func(variables []string) { app.storage.customEmails.EmailConfirmation.Variables = variables }
+		values = app.email.confirmationValues("xxxxxx", username, "xxxxxx", app, false)
 		// app.storage.customEmails.EmailConfirmation = content
 	} else {
 		respondBool(400, false, gc)
@@ -1577,7 +1509,12 @@ func (app *appContext) GetEmail(gc *gin.Context) {
 		respondBool(500, false, gc)
 		return
 	}
-	gc.JSON(200, customEmail{Content: content, Variables: variables})
+	email, err := app.email.constructTemplate("", "<div id=\"preview-content\"></div>", app)
+	if err != nil {
+		respondBool(500, false, gc)
+		return
+	}
+	gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Values: values, HTML: email.HTML})
 }
 
 // @Summary Logout by deleting refresh token from cookies.
diff --git a/email.go b/email.go
index 3ad3772..2e77af6 100644
--- a/email.go
+++ b/email.go
@@ -212,11 +212,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
 	return
 }
 
-func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) {
-	email := &Email{
-		Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
-	}
-	var err error
+func (emailer *Emailer) confirmationValues(code, username, key string, app *appContext, noSub bool) map[string]interface{} {
 	template := map[string]interface{}{
 		"clickBelow":    emailer.lang.EmailConfirmation.get("clickBelow"),
 		"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
@@ -238,6 +234,15 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
 		template["confirmationURL"] = inviteLink
 		template["message"] = message
 	}
+	return template
+}
+
+func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) {
+	email := &Email{
+		Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
+	}
+	var err error
+	template := emailer.confirmationValues(code, username, key, app, noSub)
 	if app.storage.customEmails.EmailConfirmation.Enabled {
 		content := app.storage.customEmails.EmailConfirmation.Content
 		for _, v := range app.storage.customEmails.EmailConfirmation.Variables {
@@ -274,10 +279,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (
 	return email, nil
 }
 
-func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
-	email := &Email{
-		Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
-	}
+func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
 	expiry := invite.ValidTill
 	d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
 	message := app.config.Section("email").Key("message").String()
@@ -304,6 +306,14 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
 		template["inviteURL"] = inviteLink
 		template["message"] = message
 	}
+	return template
+}
+
+func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
+	email := &Email{
+		Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
+	}
+	template := emailer.inviteValues(code, invite, app, noSub)
 	var err error
 	if app.storage.customEmails.InviteEmail.Enabled {
 		content := app.storage.customEmails.InviteEmail.Content
@@ -323,10 +333,7 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
 	return email, nil
 }
 
-func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
-	email := &Email{
-		Subject: emailer.lang.InviteExpiry.get("title"),
-	}
+func (emailer *Emailer) expiryValues(code string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
 	expiry := app.formatDatetime(invite.ValidTill)
 	template := map[string]interface{}{
 		"inviteExpired":      emailer.lang.InviteExpiry.get("inviteExpired"),
@@ -339,7 +346,15 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
 	} else {
 		template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", tmpl{"code": template["code"].(string), "time": template["time"].(string)})
 	}
+	return template
+}
+
+func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
+	email := &Email{
+		Subject: emailer.lang.InviteExpiry.get("title"),
+	}
 	var err error
+	template := emailer.expiryValues(code, invite, app, noSub)
 	if app.storage.customEmails.InviteExpiry.Enabled {
 		content := app.storage.customEmails.InviteExpiry.Content
 		for _, v := range app.storage.customEmails.InviteExpiry.Variables {
@@ -358,10 +373,7 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
 	return email, nil
 }
 
-func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) {
-	email := &Email{
-		Subject: emailer.lang.UserCreated.get("title"),
-	}
+func (emailer *Emailer) createdValues(code, username, address string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
 	template := map[string]interface{}{
 		"nameString":         emailer.lang.Strings.get("name"),
 		"addressString":      emailer.lang.Strings.get("emailAddress"),
@@ -389,6 +401,14 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
 		template["time"] = created
 		template["notificationNotice"] = emailer.lang.UserCreated.get("notificationNotice")
 	}
+	return template
+}
+
+func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) {
+	email := &Email{
+		Subject: emailer.lang.UserCreated.get("title"),
+	}
+	template := emailer.createdValues(code, username, address, invite, app, noSub)
 	var err error
 	if app.storage.customEmails.UserCreated.Enabled {
 		content := app.storage.customEmails.UserCreated.Content
@@ -408,10 +428,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
 	return email, nil
 }
 
-func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) {
-	email := &Email{
-		Subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")),
-	}
+func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} {
 	d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
 	message := app.config.Section("email").Key("message").String()
 	template := map[string]interface{}{
@@ -438,6 +455,14 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
 		template["pin"] = pwr.Pin
 		template["message"] = message
 	}
+	return template
+}
+
+func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) {
+	email := &Email{
+		Subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")),
+	}
+	template := emailer.resetValues(pwr, app, noSub)
 	var err error
 	if app.storage.customEmails.PasswordReset.Enabled {
 		content := app.storage.customEmails.PasswordReset.Content
@@ -457,10 +482,7 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
 	return email, nil
 }
 
-func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) {
-	email := &Email{
-		Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
-	}
+func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool) map[string]interface{} {
 	template := map[string]interface{}{
 		"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
 		"reasonString":          emailer.lang.UserDeleted.get("reason"),
@@ -475,7 +497,15 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
 		template["reason"] = reason
 		template["message"] = app.config.Section("email").Key("message").String()
 	}
+	return template
+}
+
+func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) {
+	email := &Email{
+		Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
+	}
 	var err error
+	template := emailer.deletedValues(reason, app, noSub)
 	if app.storage.customEmails.UserDeleted.Enabled {
 		content := app.storage.customEmails.UserDeleted.Content
 		for _, v := range app.storage.customEmails.UserDeleted.Variables {
@@ -494,10 +524,7 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
 	return email, nil
 }
 
-func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub bool) (*Email, error) {
-	email := &Email{
-		Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
-	}
+func (emailer *Emailer) welcomeValues(username string, app *appContext, noSub bool) map[string]interface{} {
 	template := map[string]interface{}{
 		"welcome":           emailer.lang.WelcomeEmail.get("welcome"),
 		"youCanLoginWith":   emailer.lang.WelcomeEmail.get("youCanLoginWith"),
@@ -515,7 +542,15 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub
 		template["username"] = username
 		template["message"] = app.config.Section("email").Key("message").String()
 	}
+	return template
+}
+
+func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub bool) (*Email, error) {
+	email := &Email{
+		Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
+	}
 	var err error
+	template := emailer.welcomeValues(username, app, noSub)
 	if app.storage.customEmails.WelcomeEmail.Enabled {
 		content := app.storage.customEmails.WelcomeEmail.Content
 		for _, v := range app.storage.customEmails.WelcomeEmail.Variables {
diff --git a/models.go b/models.go
index 39be8c7..40acc4a 100644
--- a/models.go
+++ b/models.go
@@ -189,3 +189,10 @@ type emailSetDTO struct {
 type emailTestDTO struct {
 	Address string `json:"address"`
 }
+
+type customEmailDTO struct {
+	Content   string                 `json:"content"`
+	Variables []string               `json:"variables"`
+	Values    map[string]interface{} `json:"values"`
+	HTML      string                 `json:"html"`
+}
diff --git a/package-lock.json b/package-lock.json
index 0c79482..2d795b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,14 @@
         "regenerator-runtime": "^0.13.4"
       }
     },
+    "@ts-stack/markdown": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npm.taobao.org/@ts-stack/markdown/download/@ts-stack/markdown-1.3.0.tgz",
+      "integrity": "sha1-OdkuDifo9w6Ba3L/EzaO6HWWe+o=",
+      "requires": {
+        "tslib": "^2.0.0"
+      }
+    },
     "@types/node": {
       "version": "14.14.16",
       "resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.14.16.tgz?cache=0&sync_timestamp=1608756036972&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.14.16.tgz",
@@ -228,9 +236,9 @@
       "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY="
     },
     "esbuild": {
-      "version": "0.8.49",
-      "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.49.tgz",
-      "integrity": "sha1-PTP3GzlmYR+CLPTIOBFfP70W3vI="
+      "version": "0.8.50",
+      "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.50.tgz",
+      "integrity": "sha1-6/JP3gza0aNpeJ3W/XqCCwoB5Gw="
     },
     "escalade": {
       "version": "3.1.1",
@@ -1463,6 +1471,11 @@
         "safe-buffer": "~5.1.0"
       }
     },
+    "tslib": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npm.taobao.org/tslib/download/tslib-2.1.0.tgz?cache=0&sync_timestamp=1609887446200&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftslib%2Fdownload%2Ftslib-2.1.0.tgz",
+      "integrity": "sha1-2mCGDxwuyqVwOrfTm8Bba/mIuXo="
+    },
     "typescript": {
       "version": "4.1.3",
       "resolved": "https://registry.npm.taobao.org/typescript/download/typescript-4.1.3.tgz?cache=0&sync_timestamp=1609830171931&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-4.1.3.tgz",
diff --git a/package.json b/package.json
index 70aedd8..4c05eb7 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,9 @@
   },
   "homepage": "https://github.com/hrfee/jfa-go#readme",
   "dependencies": {
+    "@ts-stack/markdown": "^1.3.0",
     "a17t": "^0.4.0",
-    "esbuild": "^0.8.49",
+    "esbuild": "^0.8.50",
     "lodash": "^4.17.19",
     "mjml": "^4.8.0",
     "remixicon": "^2.5.0",
diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts
index 1ed804e..81840ac 100644
--- a/ts/modules/settings.ts
+++ b/ts/modules/settings.ts
@@ -1,4 +1,5 @@
 import { _get, _post, toggleLoader } from "../modules/common.js";
+import { Marked } from "@ts-stack/markdown";
 
 interface settingsBoolEvent extends Event { 
     detail: boolean;
@@ -680,6 +681,8 @@ interface Email {
 interface templateEmail {
     content: string;
     variables: string[];
+    values: { [key: string]: string };
+    html: string;
 }
 
 interface emailListEl {
@@ -691,13 +694,14 @@ class EmailEditor {
     private _currentID: string;
     private _names: { [id: string]: emailListEl };
     private _content: string;
+    private _templ: templateEmail;
     private _form = document.getElementById("form-editor") as HTMLFormElement;
     private _header = document.getElementById("header-editor") as HTMLSpanElement;
     private _variables = document.getElementById("editor-variables") as HTMLDivElement;
     private _textArea = document.getElementById("textarea-editor") as HTMLTextAreaElement;
     private _preview = document.getElementById("editor-preview") as HTMLDivElement;
-    private _timeout: number;
-    private _finishInterval = 1000;
+    // private _timeout: number;
+    // private _finishInterval = 200;
 
     insert = (textarea: HTMLTextAreaElement, text: string) => { // https://kubyshkin.name/posts/insert-text-into-textarea-at-cursor-position <3
         const isSuccess = document.execCommand("insertText", false, text);
@@ -728,23 +732,25 @@ class EmailEditor {
                 if (this._names[id] !== undefined) {
                     this._header.textContent = this._names[id].name;
                 } 
-                const templ = req.response as templateEmail;
-                this._textArea.value = templ.content;
+                this._templ = req.response as templateEmail;
+                this._textArea.value = this._templ.content;
+                this._preview.innerHTML = this._templ.html;
                 this.loadPreview();
-                this._content = templ.content;
+                this._content = this._templ.content;
                 const colors = ["info", "urge", "positive", "neutral"];
                 let innerHTML = '';
-                for (let i = 0; i < templ.variables.length; i++) {
+                for (let i = 0; i < this._templ.variables.length; i++) {
                     let ci = i % colors.length;
                     innerHTML += '<span class="button ~' + colors[ci] +' !normal mb-1" style="margin-left: 0.25rem; margin-right: 0.25rem;"></span>'
                 }
                 this._variables.innerHTML = innerHTML
                 const buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
-                for (let i = 0; i < templ.variables.length; i++) {
-                    buttons[i].innerHTML = `<span class="monospace">` + templ.variables[i] + `</span>`;
+                for (let i = 0; i < this._templ.variables.length; i++) {
+                    buttons[i].innerHTML = `<span class="monospace">` + this._templ.variables[i] + `</span>`;
                     buttons[i].onclick = () => {
-                        this.insert(this._textArea, templ.variables[i]);
-                        this._timeout = setTimeout(this.loadPreview, this._finishInterval);
+                        this.insert(this._textArea, this._templ.variables[i]);
+                        this.loadPreview();
+                        // this._timeout = setTimeout(this.loadPreview, this._finishInterval);
                     }
                 }
                 window.modals.editor.show();
@@ -752,15 +758,23 @@ class EmailEditor {
         })
     }
     loadPreview = () => {
-        _post("/config/emails/" + this._currentID + "/test", { "content": this._textArea.value }, (req: XMLHttpRequest) => {
-            if (req.readyState == 4) {
-                if (req.status != 200) {
-                    window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs"));
-                    return;
-                }
-                this._preview.innerHTML = (req.response as Email).html;
-            }
-        }, true);
+        let content = this._textArea.value;
+        for (let variable of this._templ.variables) {
+            let value = this._templ.values[variable.slice(1, -1)];
+            if (value === undefined) { value = variable; }
+            content = content.replace(new RegExp(variable, "g"), value);
+        }
+        content = Marked.parse(content)
+        document.getElementById("preview-content").innerHTML = content;
+        // _post("/config/emails/" + this._currentID + "/test", { "content": this._textArea.value }, (req: XMLHttpRequest) => {
+        //     if (req.readyState == 4) {
+        //         if (req.status != 200) {
+        //             window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs"));
+        //             return;
+        //         }
+        //         this._preview.innerHTML = (req.response as Email).html;
+        //     }
+        // }, true);
     }
 
     showList = () => {
@@ -809,12 +823,13 @@ class EmailEditor {
 
     constructor() {
         this._textArea.onkeyup = () => {
-            clearTimeout(this._timeout);
-            this._timeout = setTimeout(this.loadPreview, this._finishInterval);
-        };
-        this._textArea.onkeydown = () => {
-            clearTimeout(this._timeout);
+            // clearTimeout(this._timeout);
+            // this._timeout = setTimeout(this.loadPreview, this._finishInterval);
+            this.loadPreview();
         };
+        // this._textArea.onkeydown = () => {
+        //     clearTimeout(this._timeout);
+        // };
 
         this._form.onsubmit = (event: Event) => {
             if (this._textArea.value == this._content) {
diff --git a/ts/tsconfig.json b/ts/tsconfig.json
index 95903f5..3b3b207 100644
--- a/ts/tsconfig.json
+++ b/ts/tsconfig.json
@@ -3,6 +3,8 @@
         "outDir": "../js",
         "target": "es6",
         "lib": ["dom", "es2017"],
-        "typeRoots": ["./node_modules/@types", "./typings"]
+        "typeRoots": ["./typings", "./node_modules/@types"],
+        "moduleResolution": "node",
+        "esModuleInterop": true
     }
 }