diff --git a/cmd/handlers.go b/cmd/handlers.go index 2cd7fa9..63c6ba1 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -55,6 +55,8 @@ func initHTTPHandlers(e *echo.Echo, app *App) { return c.Render(http.StatusOK, "home", publicTpl{Title: "listmonk"}) }) g.GET(path.Join(adminRoot, ""), handleAdminPage) + g.GET(path.Join(adminRoot, "/custom.css"), serveCustomApperance("admin.custom_css")) + g.GET(path.Join(adminRoot, "/custom.js"), serveCustomApperance("admin.custom_js")) g.GET(path.Join(adminRoot, "/*"), handleAdminPage) // API endpoints. @@ -142,6 +144,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) { e.POST("/webhooks/service/:service", handleBounceWebhook) } + // /public/static/* file server is registered in initHTTPServer(). // Public subscriber facing views. e.GET("/subscription/form", handleSubscriptionFormPage) e.POST("/subscription/form", handleSubscriptionForm) @@ -161,6 +164,10 @@ func initHTTPHandlers(e *echo.Echo, app *App) { "campUUID", "subUUID"))) e.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView, "campUUID", "subUUID"))) + + e.GET("/public/custom.css", serveCustomApperance("public.custom_css")) + e.GET("/public/custom.js", serveCustomApperance("public.custom_js")) + // Public health API endpoint. e.GET("/health", handleHealthCheck) } @@ -182,6 +189,39 @@ func handleHealthCheck(c echo.Context) error { return c.JSON(http.StatusOK, okResp{true}) } +// serveCustomApperance serves the given custom CSS/JS apperance blob +// meant for customizing public and admin pages from the admin settings UI. +func serveCustomApperance(name string) echo.HandlerFunc { + return func(c echo.Context) error { + var ( + app = c.Get("app").(*App) + + out []byte + hdr string + ) + + switch name { + case "admin.custom_css": + out = app.constants.Appearance.AdminCSS + hdr = "text/css; charset=utf-8" + + case "admin.custom_js": + out = app.constants.Appearance.AdminJS + hdr = "application/javascript; charset=utf-8" + + case "public.custom_css": + out = app.constants.Appearance.PublicCSS + hdr = "text/css; charset=utf-8" + + case "public.custom_js": + out = app.constants.Appearance.PublicJS + hdr = "application/javascript; charset=utf-8" + } + + return c.Blob(http.StatusOK, hdr, out) + } +} + // basicAuth middleware does an HTTP BasicAuth authentication for admin handlers. func basicAuth(username, password string, c echo.Context) (bool, error) { app := c.Get("app").(*App) diff --git a/cmd/init.go b/cmd/init.go index 8cd6d40..179d1da 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -68,6 +68,13 @@ type constants struct { AdminUsername []byte `koanf:"admin_username"` AdminPassword []byte `koanf:"admin_password"` + Appearance struct { + AdminCSS []byte `koanf:"admin.custom_css"` + AdminJS []byte `koanf:"admin.custom_js"` + PublicCSS []byte `koanf:"public.custom_css"` + PublicJS []byte `koanf:"public.custom_js"` + } + UnsubURL string LinkTrackURL string ViewTrackURL string @@ -293,7 +300,10 @@ func initConstants() *constants { lo.Fatalf("error loading app config: %v", err) } if err := ko.Unmarshal("privacy", &c.Privacy); err != nil { - lo.Fatalf("error loading app config: %v", err) + lo.Fatalf("error loading app.privacy config: %v", err) + } + if err := ko.UnmarshalWithConf("appearance", &c.Appearance, koanf.UnmarshalConf{FlatPaths: true}); err != nil { + lo.Fatalf("error loading app.appearance config: %v", err) } c.RootURL = strings.TrimRight(c.RootURL, "/") @@ -622,7 +632,7 @@ func initHTTPServer(app *App) *echo.Echo { fSrv := app.fs.FileServer() // Public (subscriber) facing static files. - srv.GET("/public/*", echo.WrapHandler(fSrv)) + srv.GET("/public/static/*", echo.WrapHandler(fSrv)) // Admin (frontend) facing static files. srv.GET("/admin/static/*", echo.WrapHandler(fSrv)) diff --git a/cmd/settings.go b/cmd/settings.go index 323a09f..2ae9fbf 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -105,6 +105,11 @@ type settings struct { TLSSkipVerify bool `json:"tls_skip_verify"` ScanInterval string `json:"scan_interval"` } `json:"bounce.mailboxes"` + + AdminCustomCSS string `json:"appearance.admin.custom_css"` + AdminCustomJS string `json:"appearance.admin.custom_js"` + PublicCustomCSS string `json:"appearance.public.custom_css"` + PublicCustomJS string `json:"appearance.public.custom_js"` } var ( diff --git a/cmd/upgrade.go b/cmd/upgrade.go index a27bc91..46b590a 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -31,6 +31,7 @@ var migList = []migFunc{ {"v0.9.0", migrations.V0_9_0}, {"v1.0.0", migrations.V1_0_0}, {"v2.0.0", migrations.V2_0_0}, + {"v2.1.0", migrations.V2_1_0}, } // upgrade upgrades the database to the current version by running SQL migration files diff --git a/frontend/public/index.html b/frontend/public/index.html index b0b0bb0..f4ea307 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -5,6 +5,8 @@ + + <%= htmlWebpackPlugin.options.title %> diff --git a/frontend/src/assets/style.scss b/frontend/src/assets/style.scss index 0e0fc12..720b2b7 100644 --- a/frontend/src/assets/style.scss +++ b/frontend/src/assets/style.scss @@ -802,6 +802,10 @@ section.analytics { .box { margin-bottom: 30px; } + .html-editor { + height: auto; + min-height: 350px; + } } /* Logs */ diff --git a/frontend/src/components/HTMLEditor.vue b/frontend/src/components/HTMLEditor.vue index 00d28f8..7ab5077 100644 --- a/frontend/src/components/HTMLEditor.vue +++ b/frontend/src/components/HTMLEditor.vue @@ -9,6 +9,10 @@ import { colors } from '../constants'; export default { props: { value: String, + language: { + type: String, + default: 'html', + }, disabled: Boolean, }, @@ -38,7 +42,7 @@ export default { this.$refs.htmlEditor.appendChild(el); this.flask = new CodeFlask(el.shadowRoot.getElementById('area'), { - language: 'html', + language: this.$props.language, lineNumbers: false, styleParent: el.shadowRoot, readonly: this.disabled, diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 8d7891d..0f01d13 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -49,6 +49,10 @@ + + + + @@ -66,6 +70,7 @@ import MediaSettings from './settings/media.vue'; import SmtpSettings from './settings/smtp.vue'; import BounceSettings from './settings/bounces.vue'; import MessengerSettings from './settings/messengers.vue'; +import AppearanceSettings from './settings/appearance.vue'; const dummyPassword = ' '.repeat(8); @@ -78,6 +83,7 @@ export default Vue.extend({ SmtpSettings, BounceSettings, MessengerSettings, + AppearanceSettings, }, data() { diff --git a/frontend/src/views/settings/appearance.vue b/frontend/src/views/settings/appearance.vue new file mode 100644 index 0000000..aa2e078 --- /dev/null +++ b/frontend/src/views/settings/appearance.vue @@ -0,0 +1,66 @@ + + + diff --git a/frontend/vue.config.js b/frontend/vue.config.js index 1d40c67..a5950bb 100644 --- a/frontend/vue.config.js +++ b/frontend/vue.config.js @@ -29,7 +29,10 @@ module.exports = { '^/$': { target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000' }, - '^/(api|webhooks|subscription|public)': { + '^/(api|webhooks|subscription|public|health)': { + target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000' + }, + '^/(admin\/custom\.(css|js))': { target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000' } } diff --git a/i18n/cs-cz.json b/i18n/cs-cz.json index b54fb21..26fd41c 100644 --- a/i18n/cs-cz.json +++ b/i18n/cs-cz.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Odběr jste zrušili úspěšně.", "public.unsubbedTitle": "Zrušen odběr", "public.unsubscribeTitle": "Zrušit odběr ze seznamu adresátů", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Akce", "settings.bounces.blocklist": "Seznam blokovaných", "settings.bounces.count": "Počet případů nedoručitelnosti", diff --git a/i18n/de.json b/i18n/de.json index f164464..663fc25 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Du wurdest erfolgreich abgemeldet", "public.unsubbedTitle": "Abgemeldet", "public.unsubscribeTitle": "Von E-Mail Liste abmelden.", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Aktion", "settings.bounces.blocklist": "Sperrliste", "settings.bounces.count": "Bounce Anzahl", diff --git a/i18n/en.json b/i18n/en.json index 99d9c49..c578918 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "You have unsubscribed successfully.", "public.unsubbedTitle": "Unsubscribed", "public.unsubscribeTitle": "Unsubscribe from mailing list", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Blocklist", "settings.bounces.count": "Bounce count", diff --git a/i18n/es.json b/i18n/es.json index 3abcf8b..865922d 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Ud. se ha des-subscrito de forma satisfactoria", "public.unsubbedTitle": "Des-subscrito.", "public.unsubscribeTitle": "Des-subscribirse de una lista de correo", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Acción", "settings.bounces.blocklist": "Lista de bloqueo", "settings.bounces.count": "Conteo de rebotes", diff --git a/i18n/fr.json b/i18n/fr.json index e99a583..9d370db 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Vous vous êtes désabonné·e avec succès.", "public.unsubbedTitle": "Désabonné·e", "public.unsubscribeTitle": "Se désabonner de la liste de diffusion", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Liste de bloquage", "settings.bounces.count": "Comptage des rebonds", diff --git a/i18n/hu.json b/i18n/hu.json index 9e76551..fbf258c 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Sikeresen leiratkozott.", "public.unsubbedTitle": "Leiratkozott", "public.unsubscribeTitle": "Leiratkozás a levelezőlistáról", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Tiltólista", "settings.bounces.count": "Visszapattanások száma", diff --git a/i18n/it.json b/i18n/it.json index d55d4f3..4c96d37 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "La cancellazione è avvenuta con successo.", "public.unsubbedTitle": "Iscrizione annullata", "public.unsubscribeTitle": "Cancella l'iscrizione dalla newsletter", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Blocklist", "settings.bounces.count": "Bounce count", diff --git a/i18n/ml.json b/i18n/ml.json index f72e863..d468ced 100644 --- a/i18n/ml.json +++ b/i18n/ml.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "നിങ്ങൾ വരിക്കാരനല്ലാതായി", "public.unsubbedTitle": "വരിക്കാരനല്ലാതാകുക", "public.unsubscribeTitle": "മെയിലിങ് ലിസ്റ്റിന്റെ വരിക്കാരനല്ലാതാകുക", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Blocklist", "settings.bounces.count": "Bounce count", diff --git a/i18n/nl.json b/i18n/nl.json index 1b20a9a..961ca9d 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Je bent met succes uitgeschreven.", "public.unsubbedTitle": "Uitgeschreven", "public.unsubscribeTitle": "Uitschrijven van mailinglijst", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Actie", "settings.bounces.blocklist": "Geblokkeerd", "settings.bounces.count": "Aantal bounces", diff --git a/i18n/pl.json b/i18n/pl.json index 401cd7e..3305a4d 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Pomyślnie odsubskrybowano", "public.unsubbedTitle": "Odsubskrybowano", "public.unsubscribeTitle": "Wypisz się z listy mailingowej", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Akcja", "settings.bounces.blocklist": "Lista zablokowanych", "settings.bounces.count": "Liczba odbić", diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index e75bfc5..0351313 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Você cancelou a inscrição com sucesso.", "public.unsubbedTitle": "Inscrição cancelada", "public.unsubscribeTitle": "Cancelar inscrição na lista de e-mails", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Blocklist", "settings.bounces.count": "Bounce count", diff --git a/i18n/pt.json b/i18n/pt.json index e11cb33..3137edb 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "A sua subscrição foi cancelada com sucesso.", "public.unsubbedTitle": "Subscrição cancelada", "public.unsubscribeTitle": "Cancelar subscrição da lista de emails", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Blocklist", "settings.bounces.count": "Bounce count", diff --git a/i18n/ro.json b/i18n/ro.json index a629fc9..e3e319d 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Te-ai dezabonat cu succes.", "public.unsubbedTitle": "Dezabonat", "public.unsubscribeTitle": "Dezabonează-te de la lista de discuții", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Acțiune", "settings.bounces.blocklist": "Lista de blocare", "settings.bounces.count": "Numarul de respingeri", diff --git a/i18n/ru.json b/i18n/ru.json index 1c40fc3..9f6d473 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Вы были отписаны.", "public.unsubbedTitle": "Отписано", "public.unsubscribeTitle": "Отписаться от списков рассылки", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Blocklist", "settings.bounces.count": "Bounce count", diff --git a/i18n/tr.json b/i18n/tr.json index e9adbf0..9770280 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -295,6 +295,13 @@ "public.unsubbedInfo": "Başarı ile üyeliğinizi bitirdiniz.", "public.unsubbedTitle": "Üyelik bitirildi.", "public.unsubscribeTitle": "e-posta listesi üyeliğini bitir", + "settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.", + "settings.appearance.adminName": "Admin", + "settings.appearance.customCSS": "Custom CSS", + "settings.appearance.customJS": "Custom JavaScript", + "settings.appearance.name": "Appearance", + "settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.", + "settings.appearance.publicName": "Public", "settings.bounces.action": "Action", "settings.bounces.blocklist": "Blocklist", "settings.bounces.count": "Bounce count", diff --git a/internal/migrations/v2.1.0.go b/internal/migrations/v2.1.0.go new file mode 100644 index 0000000..a15f03b --- /dev/null +++ b/internal/migrations/v2.1.0.go @@ -0,0 +1,23 @@ +package migrations + +import ( + "github.com/jmoiron/sqlx" + "github.com/knadh/koanf" + "github.com/knadh/stuffbin" +) + +// V2_1_0 performs the DB migrations for v.2.1.0. +func V2_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error { + if _, err := db.Exec(` + INSERT INTO settings (key, value) VALUES + ('appearance.admin.custom_css', '""'), + ('appearance.admin.custom_js', '""'), + ('appearance.public.custom_css', '""'), + ('appearance.public.custom_js', '""') + ON CONFLICT DO NOTHING; + `); err != nil { + return err + } + + return nil +} diff --git a/schema.sql b/schema.sql index 0029e06..b64a1db 100644 --- a/schema.sql +++ b/schema.sql @@ -218,7 +218,11 @@ INSERT INTO settings (key, value) VALUES ('bounce.sendgrid_enabled', 'false'), ('bounce.sendgrid_key', '""'), ('bounce.mailboxes', - '[{"enabled":false, "type": "pop", "host":"pop.yoursite.com","port":995,"auth_protocol":"userpass","username":"username","password":"password","return_path": "bounce@listmonk.yoursite.com","scan_interval":"15m","tls_enabled":true,"tls_skip_verify":false}]'); + '[{"enabled":false, "type": "pop", "host":"pop.yoursite.com","port":995,"auth_protocol":"userpass","username":"username","password":"password","return_path": "bounce@listmonk.yoursite.com","scan_interval":"15m","tls_enabled":true,"tls_skip_verify":false}]'), + ('appearance.admin.custom_css', '""'), + ('appearance.admin.custom_js', '""'), + ('appearance.public.custom_css', '""'), + ('appearance.public.custom_js', '""'); -- bounces DROP TABLE IF EXISTS bounces CASCADE; diff --git a/static/public/templates/index.html b/static/public/templates/index.html index 507476c..d310d92 100644 --- a/static/public/templates/index.html +++ b/static/public/templates/index.html @@ -7,6 +7,8 @@ + + {{ if ne .FaviconURL "" }}