From 0ff52b71f2c0e970b1f0d43793c019bbed93e112 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 27 Dec 2023 11:23:52 +0100
Subject: [PATCH] [chore] Refactor HTML templates and CSS (#2480)
* [chore] Refactor HTML templates and CSS
* eslint
* ignore "Local"
* rss tests
* fiddle with OG just a tiny bit
* dick around with polls a bit more so SR stops saying "clickable"
* remove break
* oh lord
* don't lazy load avatar
* fix ogmeta tests
* clean up some cruft
* catch remaining calls to c.HTML
* fix error rendering + stack overflow in tag
* allow templating attributes
* fix indent
* set aria-hidden on status complementary content, since it's already present in the label anyway
* tidy up templating calls a little
* try to make styling a bit more consistent + readable
* fix up some remaining CSS issues
* fix up reports
---
.goreleaser.yml | 2 +
internal/api/auth/authorize.go | 30 +-
internal/api/auth/callback.go | 34 +-
internal/api/auth/oob.go | 18 +-
internal/api/auth/signin.go | 18 +-
internal/api/model/status.go | 6 +
internal/api/util/errorhandling.go | 20 +-
internal/{web => api/util}/opengraph.go | 56 +-
internal/{web => api/util}/opengraph_test.go | 20 +-
internal/api/util/template.go | 135 +++++
internal/oauth/server.go | 4 +-
internal/processing/account/rss_test.go | 2 +-
internal/router/router.go | 1 -
internal/router/template.go | 260 ++++++--
internal/router/template_test.go | 204 +++++++
internal/text/emojify.go | 91 ++-
internal/typeutils/internaltofrontend.go | 6 +
internal/typeutils/internaltorss.go | 2 +-
internal/typeutils/internaltorss_test.go | 2 +-
internal/web/about.go | 42 +-
internal/web/confirmemail.go | 55 +-
internal/web/customcss.go | 30 +-
internal/web/domain-blocklist.go | 67 ++-
internal/web/{base.go => index.go} | 43 +-
internal/web/profile.go | 41 +-
internal/web/robots.go | 2 +-
internal/web/settings-panel.go | 44 +-
internal/web/tag.go | 18 +-
internal/web/thread.go | 30 +-
internal/web/web.go | 19 +-
testrig/gin.go | 1 -
testrig/router.go | 1 -
web/source/.eslintignore | 4 +-
web/source/css/_colors.css | 8 +-
web/source/css/about.css | 39 ++
web/source/css/base.css | 560 ++++++++----------
web/source/css/index.css | 91 ++-
web/source/css/page.css | 107 ++++
web/source/css/prism.css | 5 +
web/source/css/profile.css | 220 ++++---
web/source/css/status.css | 254 ++++----
web/source/css/thread.css | 56 ++
web/source/frontend/index.js | 4 +
web/source/frontend/prism.js | 42 ++
web/source/settings/admin/reports/detail.jsx | 42 +-
web/source/settings/admin/reports/index.jsx | 2 +-
.../settings/components/fake-profile.jsx | 29 +-
web/source/settings/components/fake-toot.jsx | 33 +-
web/source/settings/style.css | 89 ++-
web/template/404.tmpl | 40 +-
web/template/about.tmpl | 220 ++++---
web/template/authorize.tmpl | 44 +-
web/template/confirmed.tmpl | 13 +-
web/template/domain-blocklist.tmpl | 62 +-
web/template/error.tmpl | 22 +-
web/template/finalize.tmpl | 59 +-
web/template/frontend.tmpl | 7 +-
web/template/header.tmpl | 122 ----
web/template/index.tmpl | 74 +--
web/template/index_apps.tmpl | 115 ++++
web/template/oob.tmpl | 14 +-
web/template/page.tmpl | 85 +++
web/template/page_footer.tmpl | 67 +++
web/template/page_header.tmpl | 72 +++
web/template/page_ogmeta.tmpl | 57 ++
web/template/page_stylesheets.tmpl | 41 ++
web/template/profile.tmpl | 242 ++++----
.../{footer.tmpl => profile_fields.tmpl} | 40 +-
web/template/sign-in.tmpl | 10 +-
web/template/status.tmpl | 154 +++--
web/template/status_attachments.tmpl | 184 +++---
web/template/status_attributes.tmpl | 55 ++
web/template/status_header.tmpl | 56 ++
web/template/status_info.tmpl | 74 +++
web/template/status_poll.tmpl | 105 ++--
web/template/tag.tmpl | 16 +-
web/template/thread.tmpl | 59 +-
77 files changed, 3262 insertions(+), 1736 deletions(-)
rename internal/{web => api/util}/opengraph.go (74%)
rename internal/{web => api/util}/opengraph_test.go (87%)
create mode 100644 internal/api/util/template.go
create mode 100644 internal/router/template_test.go
rename internal/web/{base.go => index.go} (52%)
create mode 100644 web/source/css/about.css
create mode 100644 web/source/css/page.css
create mode 100644 web/source/css/prism.css
create mode 100644 web/source/css/thread.css
create mode 100644 web/source/frontend/prism.js
delete mode 100644 web/template/header.tmpl
create mode 100644 web/template/index_apps.tmpl
create mode 100644 web/template/page.tmpl
create mode 100644 web/template/page_footer.tmpl
create mode 100644 web/template/page_header.tmpl
create mode 100644 web/template/page_ogmeta.tmpl
create mode 100644 web/template/page_stylesheets.tmpl
rename web/template/{footer.tmpl => profile_fields.tmpl} (51%)
create mode 100644 web/template/status_attributes.tmpl
create mode 100644 web/template/status_header.tmpl
create mode 100644 web/template/status_info.tmpl
diff --git a/.goreleaser.yml b/.goreleaser.yml
index a49bb32e8..8be51ded4 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -27,6 +27,8 @@ builds:
- static_build
- kvformat
- timetzdata
+ - >-
+ {{ if and (index .Env "DEBUG") (.Env.DEBUG) }}debugenv{{ end }}
env:
- CGO_ENABLED=0
goos:
diff --git a/internal/api/auth/authorize.go b/internal/api/auth/authorize.go
index 4977ae4f2..e4694de57 100644
--- a/internal/api/auth/authorize.go
+++ b/internal/api/auth/authorize.go
@@ -144,17 +144,25 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
return
}
- // the authorize template will display a form to the user where they can get some information
- // about the app that's trying to authorize, and the scope of the request.
- // They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
- c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
- "appname": app.Name,
- "appwebsite": app.Website,
- "redirect": redirect,
- "scope": scope,
- "user": acct.Username,
- "instance": instance,
- })
+ // The authorize template will display a form
+ // to the user where they can see some info
+ // about the app that's trying to authorize,
+ // and the scope of the request. They can then
+ // approve it if it looks OK to them, which
+ // will POST to the AuthorizePOSTHandler.
+ page := apiutil.WebPage{
+ Template: "authorize.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "appname": app.Name,
+ "appwebsite": app.Website,
+ "redirect": redirect,
+ "scope": scope,
+ "user": acct.Username,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
}
// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
diff --git a/internal/api/auth/callback.go b/internal/api/auth/callback.go
index 97b3ae279..d0fa78322 100644
--- a/internal/api/auth/callback.go
+++ b/internal/api/auth/callback.go
@@ -143,11 +143,17 @@ func (m *Module) CallbackGETHandler(c *gin.Context) {
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
return
}
- c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
- "instance": instance,
- "name": claims.Name,
- "preferredUsername": claims.PreferredUsername,
- })
+
+ page := apiutil.WebPage{
+ Template: "finalize.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "name": claims.Name,
+ "preferredUsername": claims.PreferredUsername,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
return
}
s.Set(sessionUserID, user.ID)
@@ -177,12 +183,18 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
- c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
- "instance": instance,
- "name": form.Name,
- "preferredUsername": form.Username,
- "error": err,
- })
+
+ page := apiutil.WebPage{
+ Template: "finalize.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "name": form.Name,
+ "preferredUsername": form.Username,
+ "error": err,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
}
// check if the username conforms to the spec
diff --git a/internal/api/auth/oob.go b/internal/api/auth/oob.go
index 5953524ab..8c7b1f2a5 100644
--- a/internal/api/auth/oob.go
+++ b/internal/api/auth/oob.go
@@ -21,7 +21,6 @@
"context"
"errors"
"fmt"
- "net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -101,10 +100,15 @@ func (m *Module) OobHandler(c *gin.Context) {
// we're done with the session now, so just clear it out
m.clearSession(s)
- c.HTML(http.StatusOK, "oob.tmpl", gin.H{
- "instance": instance,
- "user": acct.Username,
- "oobToken": oobToken,
- "scope": scope,
- })
+ page := apiutil.WebPage{
+ Template: "oob.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "user": acct.Username,
+ "oobToken": oobToken,
+ "scope": scope,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
}
diff --git a/internal/api/auth/signin.go b/internal/api/auth/signin.go
index a6b503a83..a8713d05f 100644
--- a/internal/api/auth/signin.go
+++ b/internal/api/auth/signin.go
@@ -32,8 +32,8 @@
"golang.org/x/crypto/bcrypt"
)
-// login just wraps a form-submitted username (we want an email) and password
-type login struct {
+// signIn just wraps a form-submitted username (we want an email) and password
+type signIn struct {
Email string `form:"username"`
Password string `form:"password"`
}
@@ -55,10 +55,12 @@ func (m *Module) SignInGETHandler(c *gin.Context) {
return
}
- // no idp provider, use our own funky little sign in page
- c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{
- "instance": instance,
- })
+ page := apiutil.WebPage{
+ Template: "sign-in.tmpl",
+ Instance: instance,
+ }
+
+ apiutil.TemplateWebPage(c, page)
return
}
@@ -83,7 +85,7 @@ func (m *Module) SignInGETHandler(c *gin.Context) {
func (m *Module) SignInPOSTHandler(c *gin.Context) {
s := sessions.Default(c)
- form := &login{}
+ form := &signIn{}
if err := c.ShouldBind(form); err != nil {
m.clearSession(s)
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
@@ -129,7 +131,7 @@ func (m *Module) ValidatePassword(ctx context.Context, email string, password st
}
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
- err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err)
+ err := fmt.Errorf("password hash didn't match for user %s during sign in attempt: %s", user.Email, err)
return incorrectPassword(err)
}
diff --git a/internal/api/model/status.go b/internal/api/model/status.go
index 128cd65bb..8ca41c767 100644
--- a/internal/api/model/status.go
+++ b/internal/api/model/status.go
@@ -116,6 +116,12 @@ type Status struct {
//
// swagger:ignore
WebPollOptions []WebPollOption `json:"-"`
+
+ // Status is from a local account.
+ // Always false for non-web statuses.
+ //
+ // swagger:ignore
+ Local bool `json:"-"`
}
/*
diff --git a/internal/api/util/errorhandling.go b/internal/api/util/errorhandling.go
index 8bb251040..848beff5b 100644
--- a/internal/api/util/errorhandling.go
+++ b/internal/api/util/errorhandling.go
@@ -50,10 +50,10 @@ func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context) (*api
panic(err)
}
- c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
- "instance": instance,
- "requestID": gtscontext.RequestID(ctx),
- })
+ template404Page(c,
+ instance,
+ gtscontext.RequestID(ctx),
+ )
default:
JSON(c, http.StatusNotFound, map[string]string{
"error": errWithCode.Safe(),
@@ -73,12 +73,12 @@ func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context) (
panic(err)
}
- c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
- "instance": instance,
- "code": errWithCode.Code(),
- "error": errWithCode.Safe(),
- "requestID": gtscontext.RequestID(ctx),
- })
+ templateErrorPage(c,
+ instance,
+ errWithCode.Code(),
+ errWithCode.Safe(),
+ gtscontext.RequestID(ctx),
+ )
default:
JSON(c, errWithCode.Code(), map[string]string{
"error": errWithCode.Safe(),
diff --git a/internal/web/opengraph.go b/internal/api/util/opengraph.go
similarity index 74%
rename from internal/web/opengraph.go
rename to internal/api/util/opengraph.go
index 66b6c6eea..185dc8132 100644
--- a/internal/web/opengraph.go
+++ b/internal/api/util/opengraph.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-package web
+package util
import (
"html"
@@ -28,10 +28,10 @@
const maxOGDescriptionLength = 300
-// ogMeta represents supported OpenGraph Meta tags
+// OGMeta represents supported OpenGraph Meta tags
//
// see eg https://ogp.me/
-type ogMeta struct {
+type OGMeta struct {
// vanilla og tags
Title string // og:title
Type string // og:type
@@ -56,23 +56,23 @@ type ogMeta struct {
ProfileUsername string // profile:username
}
-// ogBase returns an *ogMeta suitable for serving at
+// OGBase returns an *ogMeta suitable for serving at
// the base root of an instance. It also serves as a
// foundation for building account / status ogMeta on
// top of.
-func ogBase(instance *apimodel.InstanceV1) *ogMeta {
+func OGBase(instance *apimodel.InstanceV1) *OGMeta {
var locale string
if len(instance.Languages) > 0 {
locale = instance.Languages[0]
}
- og := &ogMeta{
+ og := &OGMeta{
Title: text.SanitizeToPlaintext(instance.Title) + " - GoToSocial",
Type: "website",
Locale: locale,
URL: instance.URI,
SiteName: instance.AccountDomain,
- Description: parseDescription(instance.ShortDescription),
+ Description: ParseDescription(instance.ShortDescription),
Image: instance.Thumbnail,
ImageAlt: instance.ThumbnailDescription,
@@ -81,15 +81,15 @@ func ogBase(instance *apimodel.InstanceV1) *ogMeta {
return og
}
-// withAccount uses the given account to build an ogMeta
+// WithAccount uses the given account to build an ogMeta
// struct specific to that account. It's suitable for serving
// at account profile pages.
-func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta {
- og.Title = parseTitle(account, og.SiteName)
+func (og *OGMeta) WithAccount(account *apimodel.Account) *OGMeta {
+ og.Title = AccountTitle(account, og.SiteName)
og.Type = "profile"
og.URL = account.URL
if account.Note != "" {
- og.Description = parseDescription(account.Note)
+ og.Description = ParseDescription(account.Note)
} else {
og.Description = `content="This GoToSocial user hasn't written a bio yet!"`
}
@@ -102,11 +102,11 @@ func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta {
return og
}
-// withStatus uses the given status to build an ogMeta
+// WithStatus uses the given status to build an ogMeta
// struct specific to that status. It's suitable for serving
// at status pages.
-func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
- og.Title = "Post by " + parseTitle(status.Account, og.SiteName)
+func (og *OGMeta) WithStatus(status *apimodel.Status) *OGMeta {
+ og.Title = "Post by " + AccountTitle(status.Account, og.SiteName)
og.Type = "article"
if status.Language != nil {
og.Locale = *status.Language
@@ -114,9 +114,9 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
og.URL = status.URL
switch {
case status.SpoilerText != "":
- og.Description = parseDescription("CW: " + status.SpoilerText)
+ og.Description = ParseDescription("CW: " + status.SpoilerText)
case status.Text != "":
- og.Description = parseDescription(status.Text)
+ og.Description = ParseDescription(status.Text)
default:
og.Description = og.Title
}
@@ -147,34 +147,38 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
return og
}
-// parseTitle parses a page title from account and accountDomain
-func parseTitle(account *apimodel.Account, accountDomain string) string {
+// AccountTitle parses a page title from account and accountDomain
+func AccountTitle(account *apimodel.Account, accountDomain string) string {
user := "@" + account.Acct + "@" + accountDomain
if len(account.DisplayName) == 0 {
return user
}
- return account.DisplayName + " (" + user + ")"
+ return account.DisplayName + ", " + user
}
-// parseDescription returns a string description which is
+// ParseDescription returns a string description which is
// safe to use as a template.HTMLAttr inside templates.
-func parseDescription(in string) string {
+func ParseDescription(in string) string {
i := text.SanitizeToPlaintext(in)
i = strings.ReplaceAll(i, "\n", " ")
i = strings.Join(strings.Fields(i), " ")
i = html.EscapeString(i)
i = strings.ReplaceAll(i, `\`, "\")
- i = trim(i, maxOGDescriptionLength)
+ i = truncate(i, maxOGDescriptionLength)
return `content="` + i + `"`
}
-// trim strings trim s to specified length
-func trim(s string, length int) string {
- if len(s) < length {
+// truncate trims given string to
+// specified length (in runes).
+func truncate(s string, l int) string {
+ r := []rune(s)
+ if len(r) < l {
+ // No need
+ // to trim.
return s
}
- return s[:length]
+ return string(r[:l]) + "..."
}
diff --git a/internal/web/opengraph_test.go b/internal/api/util/opengraph_test.go
similarity index 87%
rename from internal/web/opengraph_test.go
rename to internal/api/util/opengraph_test.go
index 06e97cdce..2ecd6a740 100644
--- a/internal/web/opengraph_test.go
+++ b/internal/api/util/opengraph_test.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-package web
+package util
import (
"fmt"
@@ -40,18 +40,18 @@ func (suite *OpenGraphTestSuite) TestParseDescription() {
for _, tt := range tests {
tt := tt
suite.Run(tt.name, func() {
- suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), parseDescription(tt.in))
+ suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), ParseDescription(tt.in))
})
}
}
func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
- baseMeta := ogBase(&apimodel.InstanceV1{
+ baseMeta := OGBase(&apimodel.InstanceV1{
AccountDomain: "example.org",
Languages: []string{"en"},
})
- accountMeta := baseMeta.withAccount(&apimodel.Account{
+ accountMeta := baseMeta.WithAccount(&apimodel.Account{
Acct: "example_account",
DisplayName: "example person!!",
URL: "https://example.org/@example_account",
@@ -59,8 +59,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
Username: "example_account",
})
- suite.EqualValues(ogMeta{
- Title: "example person!! (@example_account@example.org)",
+ suite.EqualValues(OGMeta{
+ Title: "example person!!, @example_account@example.org",
Type: "profile",
Locale: "en",
URL: "https://example.org/@example_account",
@@ -79,12 +79,12 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
}
func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
- baseMeta := ogBase(&apimodel.InstanceV1{
+ baseMeta := OGBase(&apimodel.InstanceV1{
AccountDomain: "example.org",
Languages: []string{"en"},
})
- accountMeta := baseMeta.withAccount(&apimodel.Account{
+ accountMeta := baseMeta.WithAccount(&apimodel.Account{
Acct: "example_account",
DisplayName: "example person!!",
URL: "https://example.org/@example_account",
@@ -92,8 +92,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
Username: "example_account",
})
- suite.EqualValues(ogMeta{
- Title: "example person!! (@example_account@example.org)",
+ suite.EqualValues(OGMeta{
+ Title: "example person!!, @example_account@example.org",
Type: "profile",
Locale: "en",
URL: "https://example.org/@example_account",
diff --git a/internal/api/util/template.go b/internal/api/util/template.go
new file mode 100644
index 000000000..b8c710c3c
--- /dev/null
+++ b/internal/api/util/template.go
@@ -0,0 +1,135 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package util
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+)
+
+// WebPage encapsulates variables for
+// rendering an HTML template within
+// a standard GtS "page" template.
+type WebPage struct {
+ // Name of the template for rendering
+ // the page. Eg., "example.tmpl".
+ Template string
+
+ // Instance model for rendering header,
+ // footer, and "about" information.
+ Instance *apimodel.InstanceV1
+
+ // OGMeta for rendering page
+ // "meta:og*" tags. Can be nil.
+ OGMeta *OGMeta
+
+ // Paths to CSS files to add to
+ // the page as "stylesheet" entries.
+ // Can be nil.
+ Stylesheets []string
+
+ // Paths to JS files to add to
+ // the page as "script" entries.
+ // Can be nil.
+ Javascript []string
+
+ // Extra parameters to pass to
+ // the template for rendering,
+ // eg., "account": *Account etc.
+ // Can be nil.
+ Extra map[string]any
+}
+
+// TemplateWebPage renders the given HTML template and
+// page params within the standard GtS "page" template.
+//
+// ogMeta, stylesheets, javascript, and any extra
+// properties will be provided to the template if
+// set, but can all be nil.
+func TemplateWebPage(
+ c *gin.Context,
+ page WebPage,
+) {
+ obj := map[string]any{
+ "instance": page.Instance,
+ "ogMeta": page.OGMeta,
+ "stylesheets": page.Stylesheets,
+ "javascript": page.Javascript,
+ }
+
+ for k, v := range page.Extra {
+ obj[k] = v
+ }
+
+ templatePage(c, page.Template, http.StatusOK, obj)
+}
+
+// templateErrorPage renders the given
+// HTTP code, error, and request ID
+// within the standard error template.
+func templateErrorPage(
+ c *gin.Context,
+ instance *apimodel.InstanceV1,
+ code int,
+ err string,
+ requestID string,
+) {
+ const errorTmpl = "error.tmpl"
+
+ obj := map[string]any{
+ "instance": instance,
+ "code": code,
+ "error": err,
+ "requestID": requestID,
+ }
+
+ templatePage(c, errorTmpl, code, obj)
+}
+
+// template404Page renders
+// a standard 404 page.
+func template404Page(
+ c *gin.Context,
+ instance *apimodel.InstanceV1,
+ requestID string,
+) {
+ const notFoundTmpl = "404.tmpl"
+
+ obj := map[string]any{
+ "instance": instance,
+ "requestID": requestID,
+ }
+
+ templatePage(c, notFoundTmpl, http.StatusNotFound, obj)
+}
+
+// render the given template inside
+// "page.tmpl" with the provided
+// code and template object.
+func templatePage(
+ c *gin.Context,
+ template string,
+ code int,
+ obj map[string]any,
+) {
+ const pageTmpl = "page.tmpl"
+ obj["pageContent"] = template
+ c.HTML(code, pageTmpl, obj)
+}
diff --git a/internal/oauth/server.go b/internal/oauth/server.go
index 97e6812c5..3e4519479 100644
--- a/internal/oauth/server.go
+++ b/internal/oauth/server.go
@@ -56,8 +56,8 @@
OOBTokenPath = "/oauth/oob" // #nosec G101 else we get a hardcoded credentials warning
// HelpfulAdvice is a handy hint to users;
// particularly important during the login flow
- HelpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials"
- HelpfulAdviceGrant = "If you arrived at this error during a login/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client"
+ HelpfulAdvice = "If you arrived at this error during a sign in/oauth flow, please try clearing your session cookies and signing in again; if problems persist, make sure you're using the correct credentials"
+ HelpfulAdviceGrant = "If you arrived at this error during a sign in/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client"
)
// Server wraps some oauth2 server functions in an interface, exposing only what is needed
diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go
index e528c27e0..6ae285f9e 100644
--- a/internal/processing/account/rss_test.go
+++ b/internal/processing/account/rss_test.go
@@ -39,7 +39,7 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
fmt.Println(feed)
- suite.Equal("\n \n Posts from @admin@localhost:8080\n http://localhost:8080/@admin\n Posts from @admin@localhost:8080\n Wed, 20 Oct 2021 12:36:45 +0000\n Wed, 20 Oct 2021 12:36:45 +0000\n \n open to see some puppies\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n @admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"\n \n @admin@localhost:8080\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n Wed, 20 Oct 2021 12:36:45 +0000\n \n \n \n hello world! #welcome ! first post on the instance :rainbow: !\n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n @admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"\n !]]>\n @admin@localhost:8080\n \n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n Wed, 20 Oct 2021 11:36:45 +0000\n \n \n \n", feed)
+ suite.Equal("\n \n Posts from @admin@localhost:8080\n http://localhost:8080/@admin\n Posts from @admin@localhost:8080\n Wed, 20 Oct 2021 12:36:45 +0000\n Wed, 20 Oct 2021 12:36:45 +0000\n \n open to see some puppies\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n @admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"\n \n @admin@localhost:8080\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n Wed, 20 Oct 2021 12:36:45 +0000\n \n \n \n hello world! #welcome ! first post on the instance :rainbow: !\n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n @admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"\n !]]>\n @admin@localhost:8080\n \n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n Wed, 20 Oct 2021 11:36:45 +0000\n \n \n \n", feed)
}
func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
diff --git a/internal/router/router.go b/internal/router/router.go
index f71dc97ef..b2fb7418e 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -83,7 +83,6 @@ func New(ctx context.Context) (*Router, error) {
// Attach functions used by HTML templating,
// and load HTML templates into the engine.
- LoadTemplateFunctions(engine)
if err := LoadTemplates(engine); err != nil {
return nil, err
}
diff --git a/internal/router/template.go b/internal/router/template.go
index d1f6f297c..981c3fcf4 100644
--- a/internal/router/template.go
+++ b/internal/router/template.go
@@ -18,52 +18,121 @@
package router
import (
+ "bytes"
"fmt"
"html/template"
"os"
"path/filepath"
"reflect"
+ "regexp"
"strings"
"time"
"unsafe"
"github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/render"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/regexes"
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
-const (
- justTime = "15:04"
- dateYear = "Jan 02, 2006"
- dateTime = "Jan 02, 15:04"
- dateYearTime = "Jan 02, 2006, 15:04"
- monthYear = "Jan, 2006"
- badTimestamp = "bad timestamp"
-)
-
-// LoadTemplates loads html templates for use by the given engine
+// LoadTemplates loads templates found at `web-template-base-dir`
+// into the Gin engine, or errors if templates cannot be loaded.
+//
+// The special functions "include" and "includeAttr" will be added
+// to the template funcMap for use in any template. Use these "include"
+// functions when you need to pass a template through a pipeline.
+// Otherwise, prefer the built-in "template" function.
func LoadTemplates(engine *gin.Engine) error {
templateBaseDir := config.GetWebTemplateBaseDir()
if templateBaseDir == "" {
- return fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.WebTemplateBaseDirFlag())
+ return gtserror.Newf(
+ "%s cannot be empty and must be a relative or absolute path",
+ config.WebTemplateBaseDirFlag(),
+ )
}
- templateBaseDir, err := filepath.Abs(templateBaseDir)
+ templateDirAbs, err := filepath.Abs(templateBaseDir)
if err != nil {
- return fmt.Errorf("error getting absolute path of %s: %s", templateBaseDir, err)
+ return gtserror.Newf(
+ "error getting absolute path of web-template-base-dir %s: %w",
+ templateBaseDir, err,
+ )
}
- if _, err := os.Stat(filepath.Join(templateBaseDir, "index.tmpl")); err != nil {
- return fmt.Errorf("%s doesn't seem to contain the templates; index.tmpl is missing: %w", templateBaseDir, err)
+ indexTmplPath := filepath.Join(templateDirAbs, "index.tmpl")
+ if _, err := os.Stat(indexTmplPath); err != nil {
+ return gtserror.Newf(
+ "cannot find index.tmpl in web template directory %s: %w",
+ templateDirAbs, err,
+ )
}
- engine.LoadHTMLGlob(filepath.Join(templateBaseDir, "*"))
+ // Bring base template into scope.
+ tmpl := template.New("base")
+
+ // Set additional "include" functions to render
+ // provided template name using the base template.
+ funcMap["include"] = func(name string, data any) (template.HTML, error) {
+ var buf strings.Builder
+ err := tmpl.ExecuteTemplate(&buf, name, data)
+
+ // Template was already escaped by
+ // ExecuteTemplate so we can trust it.
+ return noescape(buf.String()), err
+ }
+
+ funcMap["includeAttr"] = func(name string, data any) (template.HTMLAttr, error) {
+ var buf strings.Builder
+ err := tmpl.ExecuteTemplate(&buf, name, data)
+
+ // Template was already escaped by
+ // ExecuteTemplate so we can trust it.
+ return noescapeAttr(buf.String()), err
+ }
+
+ // Load functions into the base template, and
+ // associate other templates with base template.
+ templateGlob := filepath.Join(templateDirAbs, "*")
+ tmpl, err = tmpl.Funcs(funcMap).ParseGlob(templateGlob)
+ if err != nil {
+ return gtserror.Newf("error loading templates: %w", err)
+ }
+
+ // Almost done; teach the
+ // engine how to render.
+ engine.SetFuncMap(funcMap)
+ engine.HTMLRender = render.HTMLProduction{Template: tmpl}
+
return nil
}
+var funcMap = template.FuncMap{
+ "add": add,
+ "acctInstance": acctInstance,
+ "demojify": demojify,
+ "deref": deref,
+ "emojify": emojify,
+ "escape": escape,
+ "increment": increment,
+ "indent": indent,
+ "indentAttr": indentAttr,
+ "isNil": isNil,
+ "outdentPre": outdentPre,
+ "noescapeAttr": noescapeAttr,
+ "noescape": noescape,
+ "oddOrEven": oddOrEven,
+ "subtract": subtract,
+ "timestampPrecise": timestampPrecise,
+ "timestamp": timestamp,
+ "timestampVague": timestampVague,
+ "visibilityIcon": visibilityIcon,
+}
+
func oddOrEven(n int) string {
if n%2 == 0 {
return "even"
@@ -71,21 +140,40 @@ func oddOrEven(n int) string {
return "odd"
}
+// escape HTML escapes the given string,
+// returning a trusted template.
func escape(str string) template.HTML {
/* #nosec G203 */
return template.HTML(template.HTMLEscapeString(str))
}
+// noescape marks the given string as a
+// trusted template. The provided string
+// MUST have already passed through a
+// template or escaping function.
func noescape(str string) template.HTML {
/* #nosec G203 */
return template.HTML(str)
}
+// noescapeAttr marks the given string as a
+// trusted HTML attribute. The provided string
+// MUST have already passed through a template
+// or escaping function.
func noescapeAttr(str string) template.HTMLAttr {
/* #nosec G203 */
return template.HTMLAttr(str)
}
+const (
+ justTime = "15:04"
+ dateYear = "Jan 02, 2006"
+ dateTime = "Jan 02, 15:04"
+ dateYearTime = "Jan 02, 2006, 15:04"
+ monthYear = "Jan, 2006"
+ badTimestamp = "bad timestamp"
+)
+
func timestamp(stamp string) string {
t, err := util.ParseISO8601(stamp)
if err != nil {
@@ -127,38 +215,55 @@ func timestampVague(stamp string) string {
return t.Format(monthYear)
}
-type iconWithLabel struct {
- faIcon string
- label string
-}
-
func visibilityIcon(visibility apimodel.Visibility) template.HTML {
- var icon iconWithLabel
+ var (
+ label string
+ icon string
+ )
switch visibility {
case apimodel.VisibilityPublic:
- icon = iconWithLabel{"globe", "public"}
+ label = "public"
+ icon = "globe"
case apimodel.VisibilityUnlisted:
- icon = iconWithLabel{"unlock", "unlisted"}
+ label = "unlisted"
+ icon = "unlock"
case apimodel.VisibilityPrivate:
- icon = iconWithLabel{"lock", "private"}
+ label = "private"
+ icon = "lock"
case apimodel.VisibilityMutualsOnly:
- icon = iconWithLabel{"handshake-o", "mutuals only"}
+ label = "mutuals-only"
+ icon = "handshake-o"
case apimodel.VisibilityDirect:
- icon = iconWithLabel{"envelope", "direct"}
+ label = "direct"
+ icon = "envelope"
}
/* #nosec G203 */
- return template.HTML(fmt.Sprintf(``, icon.label, icon.faIcon))
+ return template.HTML(fmt.Sprintf(
+ ``,
+ label, icon,
+ ))
}
-// text is a template.HTML to affirm that the input of this function is already escaped
-func emojify(emojis []apimodel.Emoji, inputText template.HTML) template.HTML {
- out := text.Emojify(emojis, string(inputText))
+// emojify replaces emojis in the given
+// html fragment with suitable tags.
+//
+// The provided input must have been
+// escaped / templated already!
+func emojify(
+ emojis []apimodel.Emoji,
+ html template.HTML,
+) template.HTML {
+ return text.EmojifyWeb(emojis, html)
+}
- /* #nosec G203 */
- // (this is escaped above)
- return template.HTML(out)
+// demojify replaces emoji shortcodes in
+// the given fragment with empty strings.
+//
+// Output must then be escaped as appropriate.
+func demojify(input string) string {
+ return text.Demojify(input)
}
func acctInstance(acct string) string {
@@ -170,10 +275,79 @@ func acctInstance(acct string) string {
return ""
}
+// increment adds 1
+// to the given int.
func increment(i int) int {
return i + 1
}
+// add adds n2 to n1.
+func add(n1 int, n2 int) int {
+ return n1 + n2
+}
+
+// subtract subtracts n2 from n1.
+func subtract(n1 int, n2 int) int {
+ return n1 - n2
+}
+
+var (
+ indentRegex = regexp.MustCompile(`(?m)^`)
+ indentStr = " "
+ indentStrLen = len(indentStr)
+ indents = strings.Repeat(indentStr, 12)
+ indentPre = regexp.MustCompile(fmt.Sprintf(`(?Ums)^((?:%s)+)
.*
`, indentStr))
+)
+
+// indent appropriately indents the given html
+// by prepending each line with the indentStr.
+func indent(n int, html template.HTML) template.HTML {
+ out := indentRegex.ReplaceAllString(
+ string(html),
+ indents[:n*indentStrLen],
+ )
+ return noescape(out)
+}
+
+// indentAttr appropriately indents the given html
+// attribute by prepending each line with the indentStr.
+func indentAttr(n int, html template.HTMLAttr) template.HTMLAttr {
+ out := indentRegex.ReplaceAllString(
+ string(html),
+ indents[:n*indentStrLen],
+ )
+ return noescapeAttr(out)
+}
+
+// outdentPre outdents all `` tags in the
+// given HTML so that they render correctly in code
+// blocks, even if they were indented before.
+func outdentPre(html template.HTML) template.HTML {
+ input := string(html)
+ output := regexes.ReplaceAllStringFunc(indentPre, input,
+ func(match string, buf *bytes.Buffer) string {
+ // Reuse the regex to pull out submatches.
+ matches := indentPre.FindAllStringSubmatch(match, -1)
+ if len(matches) != 1 {
+ return match
+ }
+
+ var (
+ indented = matches[0][0]
+ indent = matches[0][1]
+ )
+
+ // Outdent everything in the inner match, add
+ // a newline at the end to make it a bit neater.
+ outdented := strings.ReplaceAll(indented, indent, "")
+
+ // Replace original match with the outdented version.
+ return strings.ReplaceAll(match, indented, outdented)
+ },
+ )
+ return noescape(output)
+}
+
// isNil will safely check if 'v' is nil without
// dealing with weird Go interface nil bullshit.
func isNil(i interface{}) bool {
@@ -193,21 +367,3 @@ func deref(i any) any {
return vOf.Elem()
}
-
-func LoadTemplateFunctions(engine *gin.Engine) {
- engine.SetFuncMap(template.FuncMap{
- "escape": escape,
- "noescape": noescape,
- "noescapeAttr": noescapeAttr,
- "oddOrEven": oddOrEven,
- "visibilityIcon": visibilityIcon,
- "timestamp": timestamp,
- "timestampVague": timestampVague,
- "timestampPrecise": timestampPrecise,
- "emojify": emojify,
- "acctInstance": acctInstance,
- "increment": increment,
- "isNil": isNil,
- "deref": deref,
- })
-}
diff --git a/internal/router/template_test.go b/internal/router/template_test.go
new file mode 100644
index 000000000..19bf759e0
--- /dev/null
+++ b/internal/router/template_test.go
@@ -0,0 +1,204 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package router
+
+import (
+ "html/template"
+ "testing"
+)
+
+func TestOutdentPre(t *testing.T) {
+ const html = template.HTML(`
+
+
+
Here's a bunch of HTML, read it and weep, weep then!
- GoToSocial only serves Public statuses via the web.
- If you reached this page by clicking on a status link,
- it's possible that the status is not Public, has been
- deleted by the author, you don't have permission to see
- it, or it just doesn't exist at all.
-
-
- If you believe this 404 was an error, you can contact
- the instance admin. Provide them with the following request
- Request ID: {{.requestID}}.
-
-
+
+
404: Not Found
+
+ GoToSocial only serves Public statuses via the web.
+
+
+ If you reached this page by clicking on a status link,
+ it's likely that the status is not Public. You can try
+ entering the status URL in your client's search bar,
+ to view the status from your account. If that doesn't
+ work, it's possible that the status has been deleted by
+ the author, you don't have permission to view it, or it
+ doesn't exist at all.
+
+
+ If you believe this 404 was an error, you can contact
+ the instance admin. Provide them with the following
+ request ID: {{- .requestID -}}.
+
+
-
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/web/template/about.tmpl b/web/template/about.tmpl
index 6579f492f..a23dfa953 100644
--- a/web/template/about.tmpl
+++ b/web/template/about.tmpl
@@ -17,105 +17,133 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
-
-
-
No description has yet been set for this instance.
+{{- end }}
+{{- end -}}
-
-
Languages
-
- {{ if .languages }}
- This instance prefers the following languages:
-
- {{range .languages}}
-
{{.}}
- {{end}}
-
- {{ else }}
- This instance does not have any preferred languages.
- {{ end }}
-
-
+{{- define "registrationLimits" -}}
+{{- if .instance.Registrations -}}
+ Registration is enabled; new signups can be submitted to this instance.
+ {{- if .instance.ApprovalRequired -}}
+ Admin approval is required for new registrations.
+ {{- else -}}
+ Admin approval is not required for registrations; new signups will be automatically approved (pending email confirmation).
+ {{- end -}}
+{{- else -}}
+ Registration is disabled; new signups are currently closed for this instance.
+{{- end -}}
+{{- end -}}
-
+{{- define "customCSSLimits" -}}
+{{- if .instance.Configuration.Accounts.AllowCustomCSS -}}
+Users are allowed to set Custom CSS for their profiles.
+{{- else -}}
+Custom CSS is not enabled for user profiles.
+{{- end -}}
+{{- end -}}
-
-
Rules
-
- {{range .instance.Rules}}
-
{{.Text}}
- {{end}}
-
-
+{{- define "statusLimits" -}}
+Statuses can contain up to
+{{- .instance.Configuration.Statuses.MaxCharacters }} characters, and
+{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments.
+{{- end -}}
-
-
Features
-
-
- Registration is
- {{if .instance.Registrations}}
- enabled{{if .instance.ApprovalRequired}}, but requires admin approval{{end}}.
- {{else}}
- disabled.
- {{end}}
-
- Users are allowed to set Custom CSS for their profiles.
-
- {{end}}
-
- Toots can contain up to {{.instance.Configuration.Statuses.MaxCharacters}} characters and
- {{.instance.Configuration.Statuses.MaxMediaAttachments}} media attachments.
-
-
- Polls can have up to {{.instance.Configuration.Polls.MaxOptions}} options, with
- {{.instance.Configuration.Polls.MaxCharactersPerOption}} characters each.
-
-
-
+{{- define "pollLimits" -}}
+Polls can have up to
+{{- .instance.Configuration.Polls.MaxOptions }} options, with
+{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option.
+{{- end -}}
-
-
Moderated servers
-
- ActivityPub instances exchange (federate) data with other instances, including accounts and toots.
- This can be prevented for specific domains by suspending them. None of their content is stored,
- and interaction with their users is blocked both ways.
- {{if .blocklistExposed}}
- View the list of suspended domains
- {{else}}
- This instance does not publically share this list.
- {{end}}
-
This instance has not yet set a contact email address.
+ {{- end }}
+
+
+
Languages
+ {{- if .languages }}
+
This instance prefers the following languages:
+
+ {{- range .languages }}
+
{{- . -}}
+ {{- end }}
+
+ {{- else }}
+
This instance does not have any preferred languages.
+ {{- end }}
+
+
+
Instance Rules
+
This instance has the following rules:
+ {{- if .instance.Rules }}
+
+ {{- range .instance.Rules }}
+
{{- .Text -}}
+ {{- end }}
+
+ {{- else }}
+
This instance has not yet set any rules.
+ {{- end }}
+
+
+
Instance Features
+
+
{{- template "registrationLimits" . -}}
+
{{- template "customCSSLimits" . -}}
+
{{- template "statusLimits" . -}}
+
{{- template "pollLimits" . -}}
+
+
+
+
Moderated servers
+
+ ActivityPub instances federate with other instances by exchanging data with them over the network.
+ Exchanged data includes things like accounts, statuses, likes, boosts, and media attachments.
+ This exchange of data can prevented for instances on specific domains via a domain block created
+ by an instance admin. When an instance is domain blocked by another instance:
+
+
+
Any existing data from the blocked instance is deleted from the storage of the instance doing the blocking.
+
Interaction between the two instances is cut off in both directions; neither instance can interact with the other.
+
No new data from the blocked instance will be created on the instance that blocks it.
+
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/web/template/authorize.tmpl b/web/template/authorize.tmpl
index ada078968..9be094137 100644
--- a/web/template/authorize.tmpl
+++ b/web/template/authorize.tmpl
@@ -17,26 +17,24 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
-
-
-
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- with . }}
+
+
+
+{{- end }}
\ No newline at end of file
diff --git a/web/template/confirmed.tmpl b/web/template/confirmed.tmpl
index 3cf5b7ac9..c1633a8fb 100644
--- a/web/template/confirmed.tmpl
+++ b/web/template/confirmed.tmpl
@@ -17,12 +17,11 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
-
-
Email Address Confirmed
-
Thanks {{.username}}! Your email address {{.email}} has been confirmed.
-
+
+
Email Address Confirmed
+
Thanks {{ .username -}}! Your email address {{- .email -}} has been confirmed.
+
-
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/web/template/domain-blocklist.tmpl b/web/template/domain-blocklist.tmpl
index def1b990e..9a21796f9 100644
--- a/web/template/domain-blocklist.tmpl
+++ b/web/template/domain-blocklist.tmpl
@@ -17,36 +17,36 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
-
-
Suspended Instances
-
- The following list of domains have been suspended by the administrator(s) of this server.
-
-
- All current and future accounts on these instances are blocked, and no more data is federated to the remote
- servers.
- This extends to subdomains, so an entry for 'example.com' includes 'social.example.com' as well.
-
+ The following list of domains have been suspended
+ by the administrator(s) of this server.
+
+
+ All current and future accounts on these instances are
+ blocked, and no more data is federated to the remote servers.
+ This extends to subdomains, so an entry for 'example.com'
+ includes 'social.example.com' as well.
+
+
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/web/template/error.tmpl b/web/template/error.tmpl
index dc0713e43..816062e27 100644
--- a/web/template/error.tmpl
+++ b/web/template/error.tmpl
@@ -17,16 +17,16 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
-
-
An error occured:
-
{{.error}}
- {{if .requestID}}
-
- Request ID:{{.requestID}}
-
- {{end}}
-
+
+
An error occured:
+
{{- .error -}}
+ {{- if .requestID }}
+
+ Request ID:{{- .requestID -}}
+
+ {{- end }}
+
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/web/template/finalize.tmpl b/web/template/finalize.tmpl
index e0d880db7..56ab677e5 100644
--- a/web/template/finalize.tmpl
+++ b/web/template/finalize.tmpl
@@ -17,34 +17,31 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
-
-
-
-{{ template "footer.tmpl" .}}
+{{- with . }}
+
+
+
+{{- end }}
\ No newline at end of file
diff --git a/web/template/frontend.tmpl b/web/template/frontend.tmpl
index 977a7fab4..8e5267f4a 100644
--- a/web/template/frontend.tmpl
+++ b/web/template/frontend.tmpl
@@ -17,9 +17,8 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
-
-
+
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/web/template/header.tmpl b/web/template/header.tmpl
deleted file mode 100644
index 2759ab804..000000000
--- a/web/template/header.tmpl
+++ /dev/null
@@ -1,122 +0,0 @@
-{{- /*
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-*/ -}}
-
-
-{{- /*
- NESTED TEMPLATE DECLARATIONS
- If some if/else macro is used multiple times, declare it once here instead.
- When invoking these nested templates, remember to pass in the values passed
- to the executing template, ie., use '{{ template "example" . }}' not
- '{{ template "example" }}', otherwise you'll end up with empty variables.
-*/ -}}
-{{ define "thumbnailType" }}{{ if .instance.ThumbnailType }}{{ .instance.ThumbnailType }}{{ else }}image/png{{ end }}{{ end }}
-{{ define "instanceTitle" }}{{ if .ogMeta }}{{ .ogMeta.Title }}{{ else }}{{ .instance.Title }} - GoToSocial{{ end }}{{ end }}
-
-{{- /*
- BOILERPLATE GOES HERE
-*/ -}}
-
-
-
-
-
-
-
-
- {{- /*
- ROBOTS META TAGS
- If this template was provided with a specific robots meta policy, use that.
- Otherwise, fall back to a default restrictive policy.
- See: https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
- */ -}}
-
-
- {{- /*
- OPEN GRAPH META TAGS
- To enable fancy previews of links to GtS posts/profiles shared via instant
- messaging, or other social media, parse out provided Open Graph meta tags.
- */ -}}
- {{ if .ogMeta -}}
- {{ if .ogMeta.Locale }}{{ end }}
-
-
-
-
-
- {{ if .ogMeta.ArticlePublisher }}
-
-
-
-
- {{ end }}
- {{ if .ogMeta.ProfileUsername }}{{ end }}
-
- {{ if .ogMeta.ImageAlt }}{{ end }}
- {{ if .ogMeta.ImageWidth }}
-
-
- {{ end }}
- {{- end }}
-
- {{- /*
- ICON
- For icon, provide a link to the instance thumbnail. If the instance admin has
- set a custom thumbnail, use the type they uploaded, else assume image/png.
- See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#icon
- */ -}}
-
-
-
-
- {{- /*
- RSS FEED
- To enable automatic rss feed discovery for feed readers, provide the 'alternate'
- link only if rss is enabled.
- See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#alternate
- */ -}}
- {{ if .rssFeed -}}
-
- {{- end }}
-
- {{- /*
- STYLESHEET STUFF
- To try to speed up rendering a little bit, offer a preload for each stylesheet.
- See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload.
- */ -}}
-
-
- {{ range .stylesheets }}{{ end }}
-
-
- {{ range .stylesheets }}{{ end }}
- {{ template "instanceTitle" . }}
-
-
-
-
\ No newline at end of file
diff --git a/web/template/index.tmpl b/web/template/index.tmpl
index 665ce7a7e..f27cf8570 100644
--- a/web/template/index.tmpl
+++ b/web/template/index.tmpl
@@ -17,61 +17,21 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
-
- home to {{.instance.Stats.user_count}} users
- who posted {{.instance.Stats.status_count}} statuses,
- federating with {{.instance.Stats.domain_count}} other instances.
-
-
-
-
- {{.instance.ShortDescription |noescape}}
-
-
-
-
- GoToSocial does not provide its own webclient, but implements the Mastodon client API.
- You can use this server through a variety of other clients:
-
-
-
-
-
-
Semaphore
-
Semaphore is a web client designed for speed and simplicity.
No short description has yet been set for this instance.
+{{- end }}
+{{- end -}}
+
+{{- with . }}
+
+
+
About this instance
+ {{- include "shortDescription" . | indent 2 }}
+ See more details
+
+ {{- include "index_apps.tmpl" . | indent 1 }}
-{{ template "footer.tmpl" .}}
+{{- end }}
\ No newline at end of file
diff --git a/web/template/index_apps.tmpl b/web/template/index_apps.tmpl
new file mode 100644
index 000000000..05a4a9517
--- /dev/null
+++ b/web/template/index_apps.tmpl
@@ -0,0 +1,115 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+*/ -}}
+
+{{- with . }}
+
+
Client applications
+
+ GoToSocial does not provide its own webclient, but implements the Mastodon client API.
+ You can use this server through a variety of other clients:
+
+
+
+
+
Semaphore is a web client designed for speed and simplicity.
+
+{{- end }}
\ No newline at end of file
diff --git a/web/template/oob.tmpl b/web/template/oob.tmpl
index 0f183c350..ff36582e7 100644
--- a/web/template/oob.tmpl
+++ b/web/template/oob.tmpl
@@ -17,12 +17,12 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
-
-
Hi {{ .user }}!
-
Here's your out-of-band token with scope "{{.scope}}", use it wisely:
- {{ .oobToken }}
-
+
+
Hi {{ .user -}}!
+
Here's your out-of-band token with scope "{{- .scope -}}", use it wisely:
+ {{- .oobToken -}}
+
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/web/template/page.tmpl b/web/template/page.tmpl
new file mode 100644
index 000000000..347caf33e
--- /dev/null
+++ b/web/template/page.tmpl
@@ -0,0 +1,85 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+*/ -}}
+
+{{- /*
+ NESTED TEMPLATE DECLARATIONS
+ If some if/else macro is used multiple times, declare it once here instead.
+ When invoking these nested templates, remember to pass in the values passed
+ to the executing template, ie., use '{{ template "example" . }}' not
+ '{{ template "example" }}', otherwise you'll end up with empty variables.
+*/ -}}
+
+{{- define "thumbnailType" -}}
+{{- if .instance.ThumbnailType -}}
+{{- .instance.ThumbnailType -}}
+{{- else -}}
+image/png
+{{- end -}}
+{{- end -}}
+
+{{- define "instanceTitle" -}}
+{{- if .ogMeta -}}
+{{- demojify .ogMeta.Title | noescape -}}
+{{- else -}}
+{{- .instance.Title }} - GoToSocial
+{{- end -}}
+{{- end -}}
+
+
+
+
+
+
+
+
+ {{- if .ogMeta }}
+ {{- include "page_ogmeta.tmpl" . | indent 2 }}
+ {{- else }}
+ {{- end }}
+ {{- if .rssFeed }}
+
+ {{- else }}
+ {{- end }}
+ {{- if .account }}
+
+ {{- else if .status }}
+
+ {{- else }}
+ {{- end }}
+
+
+
+ {{- include "page_stylesheets.tmpl" . | indent 2 }}
+ {{- range .javascript }}
+
+ {{- end }}
+ {{- template "instanceTitle" . -}}
+
+
+
+ {{- include "page_header.tmpl" . | indent 3 }}
+
+
+
+
+
\ No newline at end of file
diff --git a/web/template/page_footer.tmpl b/web/template/page_footer.tmpl
new file mode 100644
index 000000000..a00f4dfde
--- /dev/null
+++ b/web/template/page_footer.tmpl
@@ -0,0 +1,67 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+*/ -}}
+
+{{- with . }}
+
+{{- end }}
\ No newline at end of file
diff --git a/web/template/page_header.tmpl b/web/template/page_header.tmpl
new file mode 100644
index 000000000..dc727d144
--- /dev/null
+++ b/web/template/page_header.tmpl
@@ -0,0 +1,72 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+*/ -}}
+
+{{- define "thumbnailDescription" -}}
+{{- if .instance.ThumbnailDescription -}}
+{{- .instance.ThumbnailDescription -}}
+{{- else -}}
+Instance Logo
+{{- end -}}
+{{- end -}}
+
+{{- define "strapUsers" -}}
+{{- with .instance.Stats.user_count -}}
+ {{- if eq . 1 -}}
+ {{- . -}} user
+ {{- else -}}
+ {{- . -}} users
+ {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{- define "strapPosts" -}}
+{{- with .instance.Stats.status_count -}}
+ {{- if eq . 1 -}}
+ {{- . -}} post
+ {{- else -}}
+ {{- . -}} posts
+ {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{- define "strapInstances" -}}
+{{- with .instance.Stats.domain_count -}}
+ {{- if eq . 1 -}}
+ {{- . -}} other instance
+ {{- else -}}
+ {{- . -}} other instances
+ {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{- with . }}
+
+
+
+
+{{- if .showStrap }}
+
+{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/web/template/page_ogmeta.tmpl b/web/template/page_ogmeta.tmpl
new file mode 100644
index 000000000..82bb4bbfb
--- /dev/null
+++ b/web/template/page_ogmeta.tmpl
@@ -0,0 +1,57 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+*/ -}}
+
+{{- /*
+ OPEN GRAPH META TAGS
+ To enable fancy previews of links to GtS posts/profiles shared via instant
+ messaging, or other social media, parse out provided Open Graph meta tags.
+*/ -}}
+
+{{- with .ogMeta }}
+{{- if .Locale }}
+
+{{- else }}
+{{- end }}
+
+
+
+
+
+{{- if .ArticlePublisher }}
+
+
+
+
+{{- else }}
+{{- end }}
+{{- if .ProfileUsername }}
+
+{{- else }}
+{{- end }}
+
+{{- if .ImageAlt }}
+
+{{- else }}
+{{- end }}
+{{- if .ImageWidth }}
+
+
+{{- else }}
+{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/web/template/page_stylesheets.tmpl b/web/template/page_stylesheets.tmpl
new file mode 100644
index 000000000..9234607f8
--- /dev/null
+++ b/web/template/page_stylesheets.tmpl
@@ -0,0 +1,41 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+*/ -}}
+
+{{- /*
+ Order of stylesheet loading is important: _colors and base should always be loaded
+ before any other provided sheets, since the latter cascade from the former.
+
+ To try to speed up rendering a little bit, offer a preload for each stylesheet.
+ See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload.
+*/ -}}
+
+{{- with . }}
+
+
+
+{{- range .stylesheets }}
+
+{{- end }}
+
+
+
+{{- range .stylesheets }}
+
+{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/web/template/profile.tmpl b/web/template/profile.tmpl
index 4207b39e8..0b079db10 100644
--- a/web/template/profile.tmpl
+++ b/web/template/profile.tmpl
@@ -17,129 +17,123 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
-
+{{- with . }}
-
-
- {{ if .account.Header }}
-
- {{ end }}
-
-
-
-
-
-
- {{if .account.DisplayName}}
- {{emojify .account.Emojis (escape .account.DisplayName)}}
- {{else}}
- {{.account.Username}}
- {{end}}
-
- @{{.account.Username}}@{{.instance.AccountDomain}}
- {{- /* Only render account role if 1. it's present and 2. it's not equal to the standard 'user' role */ -}}
- {{ if and (.account.Role) (ne .account.Role.Name "user") }}
-
- {{ .account.Role.Name }}
-
- {{ end }}
-
-
- Profile for
- {{if .account.DisplayName}}{{.account.DisplayName}}{{else}}{{.account.Username}}{{end}}.
- Username @{{.account.Username}}, {{.instance.AccountDomain}}.
- {{ if and (.account.Role) (ne .account.Role.Name "user") }}
- Role: {{ .account.Role.Name }}
- {{ end }}
-
- {{ if .account.Note }}
- {{emojify .account.Emojis (noescape .account.Note)}}
- {{else}}
- This GoToSocial user hasn't written a bio yet!
- {{end}}
-
-
-
- Joined on {{.account.CreatedAt | timestampVague}}.
- {{.account.StatusesCount}} post{{if .account.StatusesCount | eq 1 | not}}s{{end}}.
- Followed by {{.account.FollowersCount}}.
- Following {{.account.FollowingCount}}.
-
-
-
- Joined
- Posts{{.account.StatusesCount}}
- Followed by{{.account.FollowersCount}}
- Following{{.account.FollowingCount}}
-
+ {{- else }}
+ {{- range .statuses }}
+
+ {{- include "status.tmpl" . | indent 6 }}
+
+ {{- end }}
+ {{- end }}
+
+
+
+
+
-
-{{ template "footer.tmpl" .}}
\ No newline at end of file
+{{- end }}
\ No newline at end of file
diff --git a/web/template/footer.tmpl b/web/template/profile_fields.tmpl
similarity index 51%
rename from web/template/footer.tmpl
rename to web/template/profile_fields.tmpl
index 028a27ffb..e9005d4c9 100644
--- a/web/template/footer.tmpl
+++ b/web/template/profile_fields.tmpl
@@ -17,30 +17,16 @@
// along with this program. If not, see .
*/ -}}
-
-
-
-
- {{ if .javascript }}
- {{ range .javascript }}
-
- {{ end }}
- {{ end }}
-
-
\ No newline at end of file
+{{- with . }}
+
+{{- end }}
\ No newline at end of file
diff --git a/web/template/sign-in.tmpl b/web/template/sign-in.tmpl
index e2a985b9c..916d6942f 100644
--- a/web/template/sign-in.tmpl
+++ b/web/template/sign-in.tmpl
@@ -17,10 +17,10 @@
// along with this program. If not, see .
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
-
-