mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[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
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -21,7 +21,6 @@ import (
|
||||
"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)
|
||||
}
|
||||
|
@@ -32,8 +32,8 @@ import (
|
||||
"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)
|
||||
}
|
||||
|
||||
|
@@ -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:"-"`
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -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(),
|
||||
|
184
internal/api/util/opengraph.go
Normal file
184
internal/api/util/opengraph.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"html"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||
)
|
||||
|
||||
const maxOGDescriptionLength = 300
|
||||
|
||||
// OGMeta represents supported OpenGraph Meta tags
|
||||
//
|
||||
// see eg https://ogp.me/
|
||||
type OGMeta struct {
|
||||
// vanilla og tags
|
||||
Title string // og:title
|
||||
Type string // og:type
|
||||
Locale string // og:locale
|
||||
URL string // og:url
|
||||
SiteName string // og:site_name
|
||||
Description string // og:description
|
||||
|
||||
// image tags
|
||||
Image string // og:image
|
||||
ImageWidth string // og:image:width
|
||||
ImageHeight string // og:image:height
|
||||
ImageAlt string // og:image:alt
|
||||
|
||||
// article tags
|
||||
ArticlePublisher string // article:publisher
|
||||
ArticleAuthor string // article:author
|
||||
ArticleModifiedTime string // article:modified_time
|
||||
ArticlePublishedTime string // article:published_time
|
||||
|
||||
// profile tags
|
||||
ProfileUsername string // profile:username
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var locale string
|
||||
if len(instance.Languages) > 0 {
|
||||
locale = instance.Languages[0]
|
||||
}
|
||||
|
||||
og := &OGMeta{
|
||||
Title: text.SanitizeToPlaintext(instance.Title) + " - GoToSocial",
|
||||
Type: "website",
|
||||
Locale: locale,
|
||||
URL: instance.URI,
|
||||
SiteName: instance.AccountDomain,
|
||||
Description: ParseDescription(instance.ShortDescription),
|
||||
|
||||
Image: instance.Thumbnail,
|
||||
ImageAlt: instance.ThumbnailDescription,
|
||||
}
|
||||
|
||||
return og
|
||||
}
|
||||
|
||||
// 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 = AccountTitle(account, og.SiteName)
|
||||
og.Type = "profile"
|
||||
og.URL = account.URL
|
||||
if account.Note != "" {
|
||||
og.Description = ParseDescription(account.Note)
|
||||
} else {
|
||||
og.Description = `content="This GoToSocial user hasn't written a bio yet!"`
|
||||
}
|
||||
|
||||
og.Image = account.Avatar
|
||||
og.ImageAlt = "Avatar for " + account.Username
|
||||
|
||||
og.ProfileUsername = account.Username
|
||||
|
||||
return og
|
||||
}
|
||||
|
||||
// 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 " + AccountTitle(status.Account, og.SiteName)
|
||||
og.Type = "article"
|
||||
if status.Language != nil {
|
||||
og.Locale = *status.Language
|
||||
}
|
||||
og.URL = status.URL
|
||||
switch {
|
||||
case status.SpoilerText != "":
|
||||
og.Description = ParseDescription("CW: " + status.SpoilerText)
|
||||
case status.Text != "":
|
||||
og.Description = ParseDescription(status.Text)
|
||||
default:
|
||||
og.Description = og.Title
|
||||
}
|
||||
|
||||
if !status.Sensitive && len(status.MediaAttachments) > 0 {
|
||||
a := status.MediaAttachments[0]
|
||||
|
||||
og.ImageWidth = strconv.Itoa(a.Meta.Small.Width)
|
||||
og.ImageHeight = strconv.Itoa(a.Meta.Small.Height)
|
||||
|
||||
if a.PreviewURL != nil {
|
||||
og.Image = *a.PreviewURL
|
||||
}
|
||||
|
||||
if a.Description != nil {
|
||||
og.ImageAlt = *a.Description
|
||||
}
|
||||
} else {
|
||||
og.Image = status.Account.Avatar
|
||||
og.ImageAlt = "Avatar for " + status.Account.Username
|
||||
}
|
||||
|
||||
og.ArticlePublisher = status.Account.URL
|
||||
og.ArticleAuthor = status.Account.URL
|
||||
og.ArticlePublishedTime = status.CreatedAt
|
||||
og.ArticleModifiedTime = status.CreatedAt
|
||||
|
||||
return og
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ParseDescription returns a string description which is
|
||||
// safe to use as a template.HTMLAttr inside templates.
|
||||
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 = truncate(i, maxOGDescriptionLength)
|
||||
return `content="` + i + `"`
|
||||
}
|
||||
|
||||
// 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 string(r[:l]) + "..."
|
||||
}
|
116
internal/api/util/opengraph_test.go
Normal file
116
internal/api/util/opengraph_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
)
|
||||
|
||||
type OpenGraphTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *OpenGraphTestSuite) TestParseDescription() {
|
||||
tests := []struct {
|
||||
name, in, exp string
|
||||
}{
|
||||
{name: "shellcmd", in: `echo '\e]8;;http://example.com\e\This is a link\e]8;;\e'`, exp: `echo '\e]8;;http://example.com\e\This is a link\e]8;;\e'`},
|
||||
{name: "newlines", in: "test\n\ntest\ntest", exp: "test test test"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
suite.Run(tt.name, func() {
|
||||
suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), ParseDescription(tt.in))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
|
||||
baseMeta := OGBase(&apimodel.InstanceV1{
|
||||
AccountDomain: "example.org",
|
||||
Languages: []string{"en"},
|
||||
})
|
||||
|
||||
accountMeta := baseMeta.WithAccount(&apimodel.Account{
|
||||
Acct: "example_account",
|
||||
DisplayName: "example person!!",
|
||||
URL: "https://example.org/@example_account",
|
||||
Note: "<p>This is my profile, read it and weep! Weep then!</p>",
|
||||
Username: "example_account",
|
||||
})
|
||||
|
||||
suite.EqualValues(OGMeta{
|
||||
Title: "example person!!, @example_account@example.org",
|
||||
Type: "profile",
|
||||
Locale: "en",
|
||||
URL: "https://example.org/@example_account",
|
||||
SiteName: "example.org",
|
||||
Description: "content=\"This is my profile, read it and weep! Weep then!\"",
|
||||
Image: "",
|
||||
ImageWidth: "",
|
||||
ImageHeight: "",
|
||||
ImageAlt: "Avatar for example_account",
|
||||
ArticlePublisher: "",
|
||||
ArticleAuthor: "",
|
||||
ArticleModifiedTime: "",
|
||||
ArticlePublishedTime: "",
|
||||
ProfileUsername: "example_account",
|
||||
}, *accountMeta)
|
||||
}
|
||||
|
||||
func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
|
||||
baseMeta := OGBase(&apimodel.InstanceV1{
|
||||
AccountDomain: "example.org",
|
||||
Languages: []string{"en"},
|
||||
})
|
||||
|
||||
accountMeta := baseMeta.WithAccount(&apimodel.Account{
|
||||
Acct: "example_account",
|
||||
DisplayName: "example person!!",
|
||||
URL: "https://example.org/@example_account",
|
||||
Note: "", // <- empty
|
||||
Username: "example_account",
|
||||
})
|
||||
|
||||
suite.EqualValues(OGMeta{
|
||||
Title: "example person!!, @example_account@example.org",
|
||||
Type: "profile",
|
||||
Locale: "en",
|
||||
URL: "https://example.org/@example_account",
|
||||
SiteName: "example.org",
|
||||
Description: "content=\"This GoToSocial user hasn't written a bio yet!\"",
|
||||
Image: "",
|
||||
ImageWidth: "",
|
||||
ImageHeight: "",
|
||||
ImageAlt: "Avatar for example_account",
|
||||
ArticlePublisher: "",
|
||||
ArticleAuthor: "",
|
||||
ArticleModifiedTime: "",
|
||||
ArticlePublishedTime: "",
|
||||
ProfileUsername: "example_account",
|
||||
}, *accountMeta)
|
||||
}
|
||||
|
||||
func TestOpenGraphTestSuite(t *testing.T) {
|
||||
suite.Run(t, &OpenGraphTestSuite{})
|
||||
}
|
135
internal/api/util/template.go
Normal file
135
internal/api/util/template.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
Reference in New Issue
Block a user