Browse Source

Add first-class support for themes to more easily customize templates and static assets

pull/489/head
James Mills 1 month ago
parent
commit
8eb04dcdac
Signed by: prologic GPG Key ID: AC4C014F1440EBD6
  1. 4
      .gitignore
  2. 8
      Dockerfile
  3. 8
      Dockerfile.dev
  4. 4
      Makefile
  5. 94
      Taskfile.yml
  6. 2
      cmd/yarnd/main.go
  7. 18
      internal/config.go
  8. 2
      internal/context.go
  9. 7
      internal/options.go
  10. 40
      internal/server.go
  11. 53
      internal/templates.go
  12. 0
      internal/theme/static/css/01-pico.css
  13. 0
      internal/theme/static/css/02-icss.css
  14. 0
      internal/theme/static/css/03-icons.css
  15. 0
      internal/theme/static/css/99-twtxt.css
  16. 0
      internal/theme/static/img/.gitkeep
  17. 0
      internal/theme/static/img/favicon.png
  18. 0
      internal/theme/static/js/.gitkeep
  19. 0
      internal/theme/static/js/01-umbrella.js
  20. 0
      internal/theme/static/js/02-polyfill.js
  21. 0
      internal/theme/static/js/03-twix.js
  22. 0
      internal/theme/static/js/99-twtxt.js
  23. 0
      internal/theme/static/js/ie11CustomProperties.min.js
  24. 0
      internal/theme/static/logo.svg
  25. 0
      internal/theme/templates/401.html
  26. 0
      internal/theme/templates/403.html
  27. 0
      internal/theme/templates/404.html
  28. 0
      internal/theme/templates/base.html
  29. 0
      internal/theme/templates/blogpost.html
  30. 0
      internal/theme/templates/blogs.html
  31. 0
      internal/theme/templates/bookmarks.html
  32. 0
      internal/theme/templates/conversation.html
  33. 0
      internal/theme/templates/deleteAccount.html
  34. 0
      internal/theme/templates/delete_blogpost.html
  35. 0
      internal/theme/templates/edit_blogpost.html
  36. 0
      internal/theme/templates/error.html
  37. 0
      internal/theme/templates/externalProfile.html
  38. 0
      internal/theme/templates/feeds.html
  39. 0
      internal/theme/templates/follow.html
  40. 0
      internal/theme/templates/followers.html
  41. 0
      internal/theme/templates/following.html
  42. 0
      internal/theme/templates/import.html
  43. 0
      internal/theme/templates/login.html
  44. 0
      internal/theme/templates/manageFeed.html
  45. 0
      internal/theme/templates/managePod.html
  46. 0
      internal/theme/templates/manageUsers.html
  47. 0
      internal/theme/templates/newPassword.html
  48. 0
      internal/theme/templates/page.html
  49. 0
      internal/theme/templates/partials.html
  50. 0
      internal/theme/templates/permalink.html
  51. 0
      internal/theme/templates/profile.html
  52. 0
      internal/theme/templates/register.html
  53. 0
      internal/theme/templates/report.html
  54. 0
      internal/theme/templates/resetPassword.html
  55. 0
      internal/theme/templates/settings.html
  56. 0
      internal/theme/templates/support.html
  57. 0
      internal/theme/templates/timeline.html
  58. 0
      internal/theme/templates/transferFeed.html
  59. 0
      internal/theme/templates/version.html

4
.gitignore

@ -38,8 +38,8 @@
/docs/_errors/
/docs/.jekyll-cache/
/internal/static/css/twtxt.min.css
/internal/static/js/twtxt.min.js
/internal/theme/static/css/twtxt.min.css
/internal/theme/static/js/twtxt.min.js
bench-twtxt.txt

8
Dockerfile

@ -18,15 +18,15 @@ COPY go.mod .
COPY go.sum .
# Copy static assets
COPY ./internal/static/css/* ./internal/static/css/
COPY ./internal/static/img/* ./internal/static/img/
COPY ./internal/static/js/* ./internal/static/js/
COPY ./internal/theme/static/css/* ./internal/theme/static/css/
COPY ./internal/theme/static/img/* ./internal/theme/static/img/
COPY ./internal/theme/static/js/* ./internal/theme/static/js/
# Copy pages
COPY ./internal/pages/* ./internal/pages/
# Copy templates
COPY ./internal/templates/* ./internal/templates/
COPY ./internal/theme/templates/* ./internal/theme/templates/
# Copy langs (localization / i18n)
COPY ./internal/langs/* ./internal/langs/

8
Dockerfile.dev

@ -19,15 +19,15 @@ RUN make deps
RUN go mod download
# Copy static assets
COPY ./internal/static/css/* ./internal/static/css/
COPY ./internal/static/img/* ./internal/static/img/
COPY ./internal/static/js/* ./internal/static/js/
COPY ./internal/theme/static/css/* ./internal/theme/static/css/
COPY ./internal/theme/static/img/* ./internal/theme/static/img/
COPY ./internal/theme/static/js/* ./internal/theme/static/js/
# Copy pages
COPY ./internal/pages/* ./internal/pages/
# Copy templates
COPY ./internal/templates/* ./internal/templates/
COPY ./internal/theme/templates/* ./internal/theme/templates/
# Copy langs (localization / i18n)
COPY ./internal/langs/* ./internal/langs/

4
Makefile

@ -41,8 +41,8 @@ generate:
@if [ x"$(DEBUG)" = x"1" ]; then \
echo 'Running in debug mode...'; \
else \
minify -b -o ./internal/static/css/twtxt.min.css ./internal/static/css/[0-9]*-*.css; \
minify -b -o ./internal/static/js/twtxt.min.js ./internal/static/js/[0-9]*-*.js; \
minify -b -o ./internal/theme/static/css/twtxt.min.css ./internal/theme/static/css/[0-9]*-*.css; \
minify -b -o ./internal/theme/static/js/twtxt.min.js ./internal/theme/static/js/[0-9]*-*.js; \
fi
install: build

94
Taskfile.yml

@ -1,94 +0,0 @@
# https://taskfile.dev
version: '3'
output: 'prefixed'
vars:
YarnSvc: yard
YarnExe: yard{{exeExt}}
tasks:
default:
cmds:
- task -l
silent: true
yard:
desc: run yard service
cmds:
- task: run-yard
build-yard:
desc: build yard service
deps: [yard-css, yard-js, tr-merge]
cmds:
- echo "{{.YarnExe}} building..."
- go build -v -o {{.YarnExe}} -ldflags "-w -X {{.Module}}.Version={{.Version}} -X {{.Module}}.Commit={{.Commit}}" cmd/{{.YarnSvc}}/main.go
- echo "{{.YarnExe}} built."
generates:
- '{{.YarnExe}}'
sources:
- '**/*.go'
method: none
vars:
Commit:
sh: git rev-parse --short HEAD
Version:
sh: git describe --abbrev=0
Module:
sh: go list
silent: false
run-yard:
cmds:
- ./yard --cookie-secret abc --magiclink-secret abc --api-signing-key abc -R --debug --base-url http://localhost:8000 --bind 127.0.0.1:8000 --twts-per-page 5 --name twtxt.cc
deps: [build-yard]
desc: run yard service
silent: false
yard-css:
cmds:
- minify -b -o ./internal/static/css/twtxt.min.css ./internal/static/css/[0-9]*-*.css
sources:
- internal/css/**/*.css
generates:
- internal/static/css/twtxt.min.css
yard-js:
cmds:
- minify -b -o ./internal/static/js/twtxt.min.js ./internal/static/js/[0-9]*-*.js
sources:
- internal/js/**/*.js
generates:
- internal/static/js/twtxt.min.js
tr:
dir: internal/langs
cmds:
- goi18n merge active.*.toml
desc: goi18n create
tr-merge:
dir: internal/langs
cmds:
- goi18n merge active.*.toml translate.*.toml
desc: goi18n merge
release:
desc: release
deps: [build-yard]
cmds:
# - mkdir -pv ./release/internal/{static,langs}
- mkdir -pv ./release/internal/static/{css,js}
- cp -Rpfv ./{{.YarnExe}} ./release/
- cp -Rpfv ./internal/static/css/twtxt.min.css ./release/internal/static/css/
- cp -Rpfv ./internal/static/js/twtxt.min.js ./release/internal/static/js/
# - cp -Rpfv ./internal/langs/active.*.toml ./release/internal/langs/
- ./release/{{.YarnExe}} --cookie-secret abc --magiclink-secret abc --api-signing-key abc -R --base-url http://localhost:8000 --bind 127.0.0.1:8000 --name twtxt.cc
clean:
desc: clean
cmds:
- rm -rf ./release

2
cmd/yarnd/main.go

@ -113,7 +113,7 @@ func init() {
flag.StringVarP(&description, "description", "m", internal.DefaultMetaDescription, "set the pod's description")
flag.StringVarP(&data, "data", "d", internal.DefaultData, "data directory")
flag.StringVarP(&store, "store", "s", internal.DefaultStore, "store to use")
flag.StringVarP(&theme, "theme", "t", internal.DefaultTheme, "set the default theme")
flag.StringVarP(&theme, "theme", "t", internal.DefaultTheme, "set the theme to use for templates and static assets (if not specified, uses builtin theme)")
flag.StringVarP(&lang, "lang", "l", internal.DefaultLang, "set the default language")
flag.StringVarP(&baseURL, "base-url", "u", internal.DefaultBaseURL, "base url to use")

18
internal/config.go

@ -3,6 +3,7 @@ package internal
import (
"errors"
"fmt"
"io/fs"
"io/ioutil"
"math/rand"
"net/url"
@ -197,6 +198,23 @@ func (c *Config) Validate() error {
return nil
}
func (c *Config) TemplatesFS() fs.FS {
if c.Theme == "" {
if c.Debug {
return os.DirFS("./internal/theme/templates")
}
xs, _ := fs.Glob(builtinThemeFS, "*")
log.Infof("xs: %q", xs)
templatesFS, err := fs.Sub(builtinThemeFS, "theme/templates")
if err != nil {
log.WithError(err).Fatalf("error loading builtin theme templates")
}
return templatesFS
}
return os.DirFS(c.Theme)
}
// LoadSettings loads pod settings from the given path
func LoadSettings(path string) (*Settings, error) {
var settings Settings

2
internal/context.go

@ -76,7 +76,7 @@ type Context struct {
Message string
Lang string // language
AcceptLangs string // accept languages
Theme string
Theme string // not to be confused with the config.Theme
Commit string
Page string

7
internal/options.go

@ -45,8 +45,9 @@ const (
DefaultMetaKeywords = "twtxt, twt, blog, microBlogging, social, media, decentralised, pod"
DefaultMetaDescription = "🧶 Yarn.social is a Self-Hosted, Twitter™-like Decentralised microBlogging platform. No ads, no tracking, your content, your data!"
// DefaultTheme is the default theme to use ('light' or 'dark')
DefaultTheme = "auto"
// DefaultTheme is the default theme to use for templates and static assets
// (en empty value means to use the builtin default theme)
DefaultTheme = ""
// DefaultLang is the default language to use ('en' or 'zh-cn')
DefaultLang = "auto"
@ -274,7 +275,7 @@ func WithDescription(description string) Option {
}
}
// WithTheme sets the default theme to use
// WithTheme sets the theme to use for templates and static asssets
func WithTheme(theme string) Option {
return func(cfg *Config) error {
cfg.Theme = theme

40
internal/server.go

@ -36,14 +36,8 @@ var (
metrics *observe.Metrics
webmentions *webmention.WebMention
//go:embed static/css
staticCSS embed.FS
//go:embed static/js
staticJS embed.FS
//go:embed static/img
staticIMG embed.FS
//go:embed theme
builtinThemeFS embed.FS
)
func init() {
@ -448,22 +442,39 @@ func (s *Server) runStartupJobs() {
}
func (s *Server) initRoutes() {
var (
staticDir string
staticFS fs.FS
err error
)
if s.config.Theme == "" {
staticDir = "./internal/theme/static"
staticFS, err = fs.Sub(builtinThemeFS, "theme/static")
if err != nil {
log.WithError(err).Fatalf("error loading builtin theme static assets")
}
} else {
staticDir = s.config.Theme
staticFS = os.DirFS(staticDir)
}
if s.config.Debug {
s.router.ServeFiles("/css/*filepath", http.Dir("./internal/static/css"))
s.router.ServeFiles("/img/*filepath", http.Dir("./internal/static/img"))
s.router.ServeFiles("/js/*filepath", http.Dir("./internal/static/js"))
s.router.ServeFiles("/css/*filepath", http.Dir(filepath.Join(staticDir, "css")))
s.router.ServeFiles("/img/*filepath", http.Dir(filepath.Join(staticDir, "img")))
s.router.ServeFiles("/js/*filepath", http.Dir(filepath.Join(staticDir, "js")))
} else {
cssFS, err := fs.Sub(staticCSS, "static/css")
cssFS, err := fs.Sub(staticFS, "css")
if err != nil {
log.Fatal("error getting SubFS for static/css")
}
jsFS, err := fs.Sub(staticJS, "static/js")
jsFS, err := fs.Sub(staticFS, "js")
if err != nil {
log.Fatal("error getting SubFS for static/js")
}
imgFS, err := fs.Sub(staticIMG, "static/img")
imgFS, err := fs.Sub(staticFS, "img")
if err != nil {
log.Fatal("error getting SubFS for static/img")
}
@ -823,6 +834,7 @@ func NewServer(bind string, options ...Option) (*Server, error) {
log.Infof("Debug: %t", server.config.Debug)
log.Infof("Instance Name: %s", server.config.Name)
log.Infof("Base URL: %s", server.config.BaseURL)
log.Infof("Using Theme: %s", server.config.Theme)
log.Infof("Admin User: %s", server.config.AdminUser)
log.Infof("Admin Name: %s", server.config.AdminName)
log.Infof("Admin Email: %s", server.config.AdminEmail)

53
internal/templates.go

@ -2,12 +2,10 @@ package internal
import (
"bytes"
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
@ -22,23 +20,21 @@ import (
const (
baseTemplate = "base.html"
partialsTemplate = "_partials.html"
partialsTemplate = "partials.html"
baseName = "base"
)
//go:embed templates/*.html
var templates embed.FS
type TemplateManager struct {
sync.RWMutex
debug bool
templates map[string]*template.Template
funcMap template.FuncMap
debug bool
tmplFS fs.FS
tmplMap map[string]*template.Template
funcMap template.FuncMap
}
func NewTemplateManager(conf *Config, translator *Translator, blogs *BlogsCache, cache *Cache, archive Archiver) (*TemplateManager, error) {
templates := make(map[string]*template.Template)
tmplMap := make(map[string]*template.Template)
funcMap := sprig.FuncMap()
@ -67,7 +63,12 @@ func NewTemplateManager(conf *Config, translator *Translator, blogs *BlogsCache,
return translator.Translate(ctx, msgid, data...)
}
m := &TemplateManager{debug: conf.Debug, templates: templates, funcMap: funcMap}
m := &TemplateManager{
debug: conf.Debug,
tmplFS: conf.TemplatesFS(),
tmplMap: tmplMap,
funcMap: funcMap,
}
if err := m.LoadTemplates(); err != nil {
log.WithError(err).Error("error loading templates")
@ -81,20 +82,10 @@ func (m *TemplateManager) LoadTemplates() error {
m.Lock()
defer m.Unlock()
var (
dirFS fs.FS
root string
)
if m.debug {
dirFS = os.DirFS("./internal/templates")
root = "."
} else {
dirFS = templates
root = "templates"
}
xs, _ := fs.Glob(m.tmplFS, "*")
log.Infof("xs: %q", xs)
err := fs.WalkDir(dirFS, root, func(path string, info fs.DirEntry, err error) error {
err := fs.WalkDir(m.tmplFS, ".", func(path string, info fs.DirEntry, err error) error {
if err != nil {
log.WithError(err).Error("error walking templates")
return fmt.Errorf("error walking templates: %w", err)
@ -102,7 +93,7 @@ func (m *TemplateManager) LoadTemplates() error {
fname := info.Name()
if !info.IsDir() && filepath.Base(path) != baseTemplate {
// Skip _partials.html and also editor swap files, to improve the development
// Skip partials.html and also editor swap files, to improve the development
// cycle. Editors often add suffixes to their swap files, e.g "~" or ".swp"
// (Vim) and those files are not parsable as templates, causing panics.
if fname == partialsTemplate || !strings.HasSuffix(fname, ".html") {
@ -113,25 +104,25 @@ func (m *TemplateManager) LoadTemplates() error {
t := template.New(name).Option("missingkey=zero")
t.Funcs(m.funcMap)
if f, err := fs.ReadFile(dirFS, path); err == nil {
if f, err := fs.ReadFile(m.tmplFS, path); err == nil {
template.Must(t.Parse(string(f)))
} else {
return fmt.Errorf("error parsing template %s: %w", path, err)
}
if f, err := fs.ReadFile(dirFS, filepath.Join(root, partialsTemplate)); err == nil {
if f, err := fs.ReadFile(m.tmplFS, partialsTemplate); err == nil {
template.Must(t.Parse(string(f)))
} else {
return fmt.Errorf("error parsing partials template %s: %w", partialsTemplate, err)
}
if f, err := fs.ReadFile(dirFS, filepath.Join(root, baseTemplate)); err == nil {
if f, err := fs.ReadFile(m.tmplFS, baseTemplate); err == nil {
template.Must(t.Parse(string(f)))
} else {
return fmt.Errorf("error parsing base template %s: %w", baseTemplate, err)
}
m.templates[name] = t
m.tmplMap[name] = t
}
return nil
})
@ -146,7 +137,7 @@ func (m *TemplateManager) Add(name string, template *template.Template) {
m.Lock()
defer m.Unlock()
m.templates[name] = template
m.tmplMap[name] = template
}
func (m *TemplateManager) Exec(name string, ctx *Context) (io.WriterTo, error) {
@ -159,7 +150,7 @@ func (m *TemplateManager) Exec(name string, ctx *Context) (io.WriterTo, error) {
}
m.RLock()
template, ok := m.templates[name]
template, ok := m.tmplMap[name]
m.RUnlock()
if !ok {

0
internal/static/css/01-pico.css → internal/theme/static/css/01-pico.css

0
internal/static/css/02-icss.css → internal/theme/static/css/02-icss.css

0
internal/static/css/03-icons.css → internal/theme/static/css/03-icons.css

0
internal/static/css/99-twtxt.css → internal/theme/static/css/99-twtxt.css

0
internal/static/img/.gitkeep → internal/theme/static/img/.gitkeep

0
internal/static/img/favicon.png → internal/theme/static/img/favicon.png

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

0
internal/static/js/.gitkeep → internal/theme/static/js/.gitkeep

0
internal/static/js/01-umbrella.js → internal/theme/static/js/01-umbrella.js

0
internal/static/js/02-polyfill.js → internal/theme/static/js/02-polyfill.js

0
internal/static/js/03-twix.js → internal/theme/static/js/03-twix.js

0
internal/static/js/99-twtxt.js → internal/theme/static/js/99-twtxt.js

0
internal/static/js/ie11CustomProperties.min.js → internal/theme/static/js/ie11CustomProperties.min.js

0
internal/static/logo.svg → internal/theme/static/logo.svg

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

0
internal/templates/401.html → internal/theme/templates/401.html

0
internal/templates/403.html → internal/theme/templates/403.html

0
internal/templates/404.html → internal/theme/templates/404.html

0
internal/templates/base.html → internal/theme/templates/base.html

0
internal/templates/blogpost.html → internal/theme/templates/blogpost.html

0
internal/templates/blogs.html → internal/theme/templates/blogs.html

0
internal/templates/bookmarks.html → internal/theme/templates/bookmarks.html

0
internal/templates/conversation.html → internal/theme/templates/conversation.html

0
internal/templates/deleteAccount.html → internal/theme/templates/deleteAccount.html

0
internal/templates/delete_blogpost.html → internal/theme/templates/delete_blogpost.html

0
internal/templates/edit_blogpost.html → internal/theme/templates/edit_blogpost.html

0
internal/templates/error.html → internal/theme/templates/error.html

0
internal/templates/externalProfile.html → internal/theme/templates/externalProfile.html

0
internal/templates/feeds.html → internal/theme/templates/feeds.html

0
internal/templates/follow.html → internal/theme/templates/follow.html

0
internal/templates/followers.html → internal/theme/templates/followers.html

0
internal/templates/following.html → internal/theme/templates/following.html

0
internal/templates/import.html → internal/theme/templates/import.html

0
internal/templates/login.html → internal/theme/templates/login.html

0
internal/templates/manageFeed.html → internal/theme/templates/manageFeed.html

0
internal/templates/managePod.html → internal/theme/templates/managePod.html

0
internal/templates/manageUsers.html → internal/theme/templates/manageUsers.html

0
internal/templates/newPassword.html → internal/theme/templates/newPassword.html

0
internal/templates/page.html → internal/theme/templates/page.html

0
internal/templates/_partials.html → internal/theme/templates/partials.html

0
internal/templates/permalink.html → internal/theme/templates/permalink.html

0
internal/templates/profile.html → internal/theme/templates/profile.html

0
internal/templates/register.html → internal/theme/templates/register.html

0
internal/templates/report.html → internal/theme/templates/report.html

0
internal/templates/resetPassword.html → internal/theme/templates/resetPassword.html

0
internal/templates/settings.html → internal/theme/templates/settings.html

0
internal/templates/support.html → internal/theme/templates/support.html

0
internal/templates/timeline.html → internal/theme/templates/timeline.html

0
internal/templates/transferFeed.html → internal/theme/templates/transferFeed.html

0
internal/templates/version.html → internal/theme/templates/version.html

Loading…
Cancel
Save