Author SHA1 Message Date
schreifuchs 01b24f830f chore: better resource loading
/ publish (push) Successful in 9m23s
2026-03-08 01:05:57 +01:00
schreifuchs eab7eb3879 chore: better config
/ publish (push) Successful in 3m1s
2026-03-07 20:55:04 +01:00
schreifuchs fca1030e1a feat: limit image request
/ publish (push) Successful in 3m9s
2026-03-07 20:26:53 +01:00
schreifuchs 2f7ff28c9a feat: better loging
/ publish (push) Successful in 3m7s
2026-03-07 19:31:19 +01:00
schreifuchs 82e601fd7f chore: enable webp support
/ publish (push) Successful in 3m22s
2026-03-07 18:23:18 +01:00
schreifuchs f5bcc7c381 feat: configuration
/ publish (push) Successful in 3m27s
2026-03-07 16:58:19 +01:00
schreifuchs 4efd3ee396 Merge pull request 'rewrite/go-templ' (#1) from rewrite/go-templ into main
/ publish (push) Successful in 3m40s
Reviewed-on: #1
2026-03-03 06:47:07 +01:00
39 changed files with 2244 additions and 3534 deletions
+1
View File
@@ -1,6 +1,7 @@
root = "." root = "."
testdata_dir = "testdata" testdata_dir = "testdata"
tmp_dir = "tmp" tmp_dir = "tmp"
env_files = [".env"]
[build] [build]
args_bin = [] args_bin = []
+1 -2
View File
@@ -8,6 +8,5 @@ build
docker-compose.yml docker-compose.yml
Dockerfile Dockerfile
README.md README.md
TODO_NEXTCLOUD.md
GEMINI.md
pnpm-workspace.yaml pnpm-workspace.yaml
+28 -6
View File
@@ -1,22 +1,44 @@
package main package main
import ( import (
"context"
"log/slog" "log/slog"
"net/http"
"os" "os"
"os/signal"
_ "net/http/pprof"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/server" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/server"
) )
func main() { func main() {
server := server.NewServer() go func() {
http.ListenAndServe("localhost:6060", nil)
}()
ctx := context.Background()
ctx, cancle := context.WithCancel(ctx)
defer cancle()
go func() {
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt)
<-sigint
cancle()
}()
slog.SetLogLoggerLevel(slog.LevelDebug) slog.SetLogLoggerLevel(slog.LevelDebug)
slog.Info("Server started", "addr", server.Addr)
err := server.ListenAndServe() cfg, err := config.Read(ctx)
if err != nil { if err != nil {
slog.Error("Server failed to start", "error", err) slog.Error("could not read configuaration", "err", err)
os.Exit(1) }
err = server.Start(ctx, cfg)
if err != nil {
slog.Error("error while serving", "err", err)
return
} }
} }
+1
View File
@@ -8,6 +8,7 @@ require (
github.com/a-h/templ v0.3.977 github.com/a-h/templ v0.3.977
github.com/alicebob/miniredis/v2 v2.36.1 github.com/alicebob/miniredis/v2 v2.36.1
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab
github.com/sethvargo/go-envconfig v1.3.0
github.com/studio-b12/gowebdav v0.12.0 github.com/studio-b12/gowebdav v0.12.0
github.com/valkey-io/valkey-go v1.0.72 github.com/valkey-io/valkey-go v1.0.72
golang.org/x/image v0.36.0 golang.org/x/image v0.36.0
+2
View File
@@ -31,6 +31,8 @@ github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U=
github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM= github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
+10 -2
View File
@@ -10,12 +10,13 @@ import (
"hash/fnv" "hash/fnv"
"image" "image"
"image/jpeg" "image/jpeg"
_ "image/jpeg"
_ "image/png" _ "image/png"
"io" "io"
"strings" "strings"
"time" "time"
_ "golang.org/x/image/webp"
"github.com/valkey-io/valkey-go" "github.com/valkey-io/valkey-go"
) )
@@ -59,6 +60,13 @@ func (s *Service) getImage(uid string) (file []byte, mimeType string, err error)
} }
func (s *Service) GetImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) { func (s *Service) GetImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) {
if err = s.seph.Acquire(ctx, 1); err != nil {
err = fmt.Errorf("could not Acquire semaphore: %w", err)
return
}
defer s.seph.Release(1)
rawImage, mimeType, err := s.getImage(uid) rawImage, mimeType, err := s.getImage(uid)
if err != nil { if err != nil {
return return
@@ -82,7 +90,7 @@ func (s *Service) GetImage(ctx context.Context, uid string, options Options) (im
outBuff := bytes.NewBuffer(make([]byte, 0, 1024)) outBuff := bytes.NewBuffer(make([]byte, 0, 1024))
if options.Quality == 0 { if options.Quality == 0 {
options.Quality = 80 options.Quality = s.cfg.Quality
} }
err = jpeg.Encode(outBuff, decImage, &jpeg.Options{Quality: options.Quality}) err = jpeg.Encode(outBuff, decImage, &jpeg.Options{Quality: options.Quality})
+7 -1
View File
@@ -3,7 +3,9 @@ package images
import ( import (
"os" "os"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
"github.com/valkey-io/valkey-go" "github.com/valkey-io/valkey-go"
"golang.org/x/sync/semaphore"
) )
type Options struct { type Options struct {
@@ -15,12 +17,16 @@ type Options struct {
type Service struct { type Service struct {
fs fileSystem fs fileSystem
cache valkey.Client cache valkey.Client
seph *semaphore.Weighted
cfg *config.ImageConfig
} }
func New(fs fileSystem, cache valkey.Client) *Service { func New(fs fileSystem, cfg *config.ImageConfig, cache valkey.Client) *Service {
return &Service{ return &Service{
fs: fs, fs: fs,
cache: cache, cache: cache,
seph: semaphore.NewWeighted(cfg.Concurency),
cfg: cfg,
} }
} }
+5 -2
View File
@@ -55,8 +55,11 @@ func imageMiddleware(page PageHeader) func(w io.Writer, node ast.Node, entering
} }
img.Attribute = &ast.Attribute{Attrs: map[string][]byte{ img.Attribute = &ast.Attribute{Attrs: map[string][]byte{
"srcset": []byte(images.SourceSet(src)), "srcset": []byte(images.SourceSet(src)),
"src": []byte(src), "fetchpriority": []byte("low"),
"loading": []byte("lazy"),
"class": []byte("min-h-48"),
"src": []byte(src),
}} }}
// // img.Attrs["srcset"] = // // img.Attrs["srcset"] =
+1 -4
View File
@@ -4,13 +4,12 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"regexp" "regexp"
"strings" "strings"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/filesystem" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem"
"github.com/studio-b12/gowebdav" "github.com/studio-b12/gowebdav"
) )
@@ -57,8 +56,6 @@ func (s *Service) getPageHeaders(ctx context.Context, uid string) (page PageHead
} }
} }
slog.Debug("loaded page infos", "cover", coverImage, "content", contentMD)
page.md, err = s.fs.Read(contentMD.Path()) page.md, err = s.fs.Read(contentMD.Path())
if err != nil { if err != nil {
return return
+1 -1
View File
@@ -1,7 +1,7 @@
package page package page
import ( import (
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/filesystem" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem"
) )
type Service struct { type Service struct {
+1 -1
View File
@@ -65,7 +65,7 @@ func PathForLang(ctx context.Context, lang string, pp ...string) string {
} }
} }
return path.Join(append([]string{lang}, pp...)...) return "/" + path.Join(append([]string{lang}, pp...)...)
} }
func (s *Service) SupportedLanguages() (languages []string) { func (s *Service) SupportedLanguages() (languages []string) {
+10
View File
@@ -0,0 +1,10 @@
package config
func Default() Cfg {
return Cfg{
Image: &ImageConfig{
Concurency: 10,
Quality: 80,
},
}
}
+56
View File
@@ -0,0 +1,56 @@
package config
import (
"context"
"github.com/sethvargo/go-envconfig"
)
type Cfg struct {
FileSystem *WebdavConfig `env:", prefix=FILESYSTEM_"`
Cache *ValkeyConfig `env:", prefix=CACHE_"`
Image *ImageConfig `env:", prefix=IMAGE_"`
}
type WebdavConfig struct {
URL string `env:"URL"`
User string `env:"USERNAME"`
Password string `env:"PASSWORD"`
}
type ValkeyConfig struct {
ClientName string `env:"CLIENTNAME"`
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
// InitAddress point to valkey nodes.
// Valkey will connect to them one by one and issue a CLUSTER SLOT command to initialize the cluster client until success.
// If len(InitAddress) == 1 and the address is not running in cluster mode, valkey will fall back to the single client mode.
// If ClientOption.Sentinel.MasterSet is set, then InitAddress will be used to connect sentinels
// You can bypass this behavior by using ClientOption.ForceSingleClient.
InitAddress []string `env:"ADDRESS"`
}
type ImageConfig struct {
Concurency int64 `env:"CONCURENCY"`
Quality int `env:"QUALITY"`
}
func Read(ctx context.Context) (cfg Cfg, err error) {
cfg = Default()
err = envconfig.ProcessWith(ctx, &envconfig.Config{
Target: &cfg,
// All fields will use a ";" delimiter and "@" separator, unless locally
// overridden.
DefaultDelimiter: ";",
DefaultSeparator: "@",
Lookuper: envconfig.MultiLookuper(secretLookuper(), envconfig.OsLookuper()),
})
if err != nil {
return
}
return
}
+36
View File
@@ -0,0 +1,36 @@
package config
import (
"io"
"log/slog"
"os"
"strings"
"github.com/sethvargo/go-envconfig"
)
func secretLookuper() envconfig.LookuperFunc {
return func(key string) (string, bool) {
fileName := os.Getenv(key + "_FILE")
if fileName == "" {
return "", false
}
file, err := os.Open(fileName)
if err != nil {
slog.Error("could not read secret file", "path", fileName)
return "", false
}
defer file.Close()
sb := &strings.Builder{}
_, err = io.Copy(sb, file)
if err != nil {
return "", false
}
return sb.String(), true
}
}
+46
View File
@@ -0,0 +1,46 @@
package server
import (
"fmt"
"log/slog"
"net/http"
"time"
)
type captureWriter struct {
http.ResponseWriter
code int
}
func (c *captureWriter) WriteHeader(statusCode int) {
c.code = statusCode
c.ResponseWriter.WriteHeader(statusCode)
}
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
cw := &captureWriter{
ResponseWriter: w,
code: 200,
}
next.ServeHTTP(cw, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start),
"status", cw.code,
)
})
}
// cacheMiddleware adds Cache-Control headers to the response.
func cacheMiddleware(maxAge time.Duration) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", maxAge.Seconds()))
w.Header().Set("Vary", "HX-Request,Accept-Language,Accept-Encoding")
next.ServeHTTP(w, r)
})
}
}
+10 -27
View File
@@ -3,16 +3,15 @@ package server
import ( import (
"log/slog" "log/slog"
"net/http" "net/http"
"os"
"time" "time"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/filesystem"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/resthome" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/resthome"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/restimage" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/restimage"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/templer" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/templer"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web"
"github.com/studio-b12/gowebdav" "github.com/studio-b12/gowebdav"
"github.com/valkey-io/valkey-go" "github.com/valkey-io/valkey-go"
@@ -22,7 +21,7 @@ func (s *Server) RegisterRoutes() (h http.Handler) {
mux := http.NewServeMux() mux := http.NewServeMux()
registerStatic(mux) registerStatic(mux)
mux.Handle("/", registerOther()) mux.Handle("/", s.registerOther())
h = mux h = mux
h = templer.Middleware(h) h = templer.Middleware(h)
@@ -30,23 +29,19 @@ func (s *Server) RegisterRoutes() (h http.Handler) {
return h return h
} }
func registerOther() (h http.Handler) { func (s *Server) registerOther() (h http.Handler) {
mux := http.NewServeMux() mux := http.NewServeMux()
webdavClient := gowebdav.NewClient("https://cloud.schreifuchs.ch/remote.php/dav/files/test/website", "test", os.Getenv("TEST_PW")) webdavClient := gowebdav.NewClient(s.cfg.FileSystem.URL, s.cfg.FileSystem.User, s.cfg.FileSystem.Password)
if err := webdavClient.Connect(); err != nil { if err := webdavClient.Connect(); err != nil {
slog.Error("could not connect webdavClient", "err", err, "pw", os.Getenv("TEST_PW")) slog.Error("could not connect webdavClient", "err", err)
} }
var fs filesystem.FS = webdavClient var fs filesystem.FS = webdavClient
valkeyAddr := os.Getenv("VALKEY_ADDR")
if valkeyAddr == "" {
valkeyAddr = "127.0.0.1:6379"
}
valkeyClient, err := valkey.NewClient(valkey.ClientOption{ valkeyClient, err := valkey.NewClient(valkey.ClientOption{
InitAddress: []string{valkeyAddr}, InitAddress: s.cfg.Cache.InitAddress,
Password: os.Getenv("VALKEY_PASSWORD"), Password: s.cfg.Cache.Password,
}) })
if err != nil { if err != nil {
slog.Error("failed to create valkey client", "err", err) slog.Error("failed to create valkey client", "err", err)
@@ -55,7 +50,7 @@ func registerOther() (h http.Handler) {
} }
_ = resthome.New(mux, page.New(fs)) _ = resthome.New(mux, page.New(fs))
_ = restimage.New(mux, images.New(fs, valkeyClient)) _ = restimage.New(mux, images.New(fs, s.cfg.Image, valkeyClient))
mux.Handle("/", http.FileServer(http.FS(web.GetStaticFS()))) mux.Handle("/", http.FileServer(http.FS(web.GetStaticFS())))
@@ -63,18 +58,6 @@ func registerOther() (h http.Handler) {
h = mux h = mux
h = translator.Middleware(h) h = translator.Middleware(h)
h = cacheMiddleware(time.Second * 45)(h) h = cacheMiddleware(time.Minute * 5)(h)
return h return h
} }
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start),
)
})
}
+30 -3
View File
@@ -1,34 +1,61 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"time" "time"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
) )
type Server struct { type Server struct {
port int port int
cfg config.Cfg
} }
func NewServer() *http.Server { func Start(ctx context.Context, cfg config.Cfg) (err error) {
port, _ := strconv.Atoi(os.Getenv("PORT")) port, _ := strconv.Atoi(os.Getenv("PORT"))
if port == 0 { if port == 0 {
port = 8080 port = 8080
} }
s := &Server{ s := &Server{
port: port, port: port,
cfg: cfg,
} }
// Declare Server config // Declare Server config
server := &http.Server{ srv := &http.Server{
Addr: fmt.Sprintf(":%d", s.port), Addr: fmt.Sprintf(":%d", s.port),
Handler: s.RegisterRoutes(), Handler: s.RegisterRoutes(),
IdleTimeout: time.Minute, IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second,
} }
idleConnsClosed := make(chan struct{})
return server go func() {
<-ctx.Done()
slog.Info("shutting down server")
// Shutdown server when ctx done
if err = srv.Shutdown(context.Background()); err != nil {
// Error from closing listeners, or context timeout:
err = fmt.Errorf("error whilse shutting down server: %w", err)
}
close(idleConnsClosed)
}()
slog.Info("starting server", "address", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
return fmt.Errorf("error while server ListenAndServe: %w", err)
}
<-idleConnsClosed
return err
} }
+1 -13
View File
@@ -1,7 +1,6 @@
package server package server
import ( import (
"fmt"
"io/fs" "io/fs"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -27,7 +26,7 @@ func registerStatic(mux *http.ServeMux) {
path = "/" + path path = "/" + path
slog.Info("registering file", "path", path) slog.Debug("registering file", "path", path)
mux.Handle(path, fileServer) mux.Handle(path, fileServer)
@@ -38,14 +37,3 @@ func registerStatic(mux *http.ServeMux) {
panic(err) panic(err)
} }
} }
// cacheMiddleware adds Cache-Control headers to the response.
func cacheMiddleware(maxAge time.Duration) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", maxAge.Seconds()))
w.Header().Set("Vary", "HX-Request,Accept-Language,Accept-Encoding")
next.ServeHTTP(w, r)
})
}
}
+2 -1
View File
@@ -6,7 +6,7 @@
"scripts": { "scripts": {
"build:css": "tailwindcss -i ./web/static/css/input.css -o ./web/static/css/output.css --minify", "build:css": "tailwindcss -i ./web/static/css/input.css -o ./web/static/css/output.css --minify",
"watch:css": "tailwindcss -i ./web/static/css/input.css -o ./web/static/css/output.css --watch", "watch:css": "tailwindcss -i ./web/static/css/input.css -o ./web/static/css/output.css --watch",
"build:js": "cp node_modules/htmx.org/dist/htmx.min.js web/static/js/htmx.min.js" "build:js": "cp node_modules/htmx.org/dist/htmx.min.js web/static/js/htmx.min.js && cp ./node_modules/htmx-ext-preload/dist/preload.min.js ./web/static/js/preload.min.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -20,6 +20,7 @@
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"dependencies": { "dependencies": {
"htmx-ext-preload": "^2.1.2",
"htmx.org": "^2.0.8" "htmx.org": "^2.0.8"
} }
} }
+10 -3464
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

+1976
View File
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 112 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

+1
View File
@@ -8,6 +8,7 @@ templ ImageTile(src string) {
src={ src } src={ src }
srcset={ images.SourceSet(src) } srcset={ images.SourceSet(src) }
fetchpriority="high" fetchpriority="high"
loading="eager"
class="row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover" class="row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover"
/> />
<div class="row-start-1 col-start-1"> <div class="row-start-1 col-start-1">
+4 -4
View File
@@ -2,7 +2,7 @@ package layouts
import "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate" import "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate"
import "strings" import "strings"
import "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/templer" import "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/templer"
templ Base(title string) { templ Base(title string) {
<!DOCTYPE html> <!DOCTYPE html>
@@ -14,17 +14,17 @@ templ Base(title string) {
<link rel="icon" type="image/png" href="/favicon.png"/> <link rel="icon" type="image/png" href="/favicon.png"/>
<link fetchpriority="high" href="/css/output.css" rel="stylesheet"/> <link fetchpriority="high" href="/css/output.css" rel="stylesheet"/>
<script fetchpriority="low" src="/js/htmx.min.js"></script> <script fetchpriority="low" src="/js/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx-ext-preload@2.1.2" integrity="sha384-PRIcY6hH1Y5784C76/Y8SqLyTanY9rnI3B8F3+hKZFNED55hsEqMJyqWhp95lgfk" crossorigin="anonymous"></script> <script src="/js/preload.min.js"></script>
</head> </head>
<body class="bg-black dark" hx-boost="true" hx-target="main" hx-ext="preload"> <body class="bg-black dark" hx-boost="true" hx-target="main" hx-ext="preload">
<header <header
class="fixed z-50 mix-blend-difference p-1 w-full flex justify-between items-start" class="fixed z-50 mix-blend-difference p-1 w-full flex justify-between items-start"
> >
<a class="text-white text-2xl" href={ "/" + translate.Path(ctx, "") }>schreifuchs.ch</a> <a class="text-white text-2xl" href={ translate.Path(ctx, "") }>schreifuchs.ch</a>
<nav class="flex gap-2 p-2"> <nav class="flex gap-2 p-2">
for _, lang := range translate.SupportedLanguages(ctx) { for _, lang := range translate.SupportedLanguages(ctx) {
<a <a
href={ "/" + translate.PathForLang(ctx, lang, strings.Split(templer.Path(ctx), "/")...) } href={ translate.PathForLang(ctx, lang, strings.Split(templer.Path(ctx), "/")...) }
hreflang={ lang } hreflang={ lang }
data-no-translate data-no-translate
class="text-white hover:underline uppercase" class="text-white hover:underline uppercase"
+1 -1
View File
@@ -19,7 +19,7 @@ func Render(ctx context.Context, w io.Writer, r *http.Request, component templ.C
slog.Debug("HX-Request", "value", r.Header.Get("HX-Request")) slog.Debug("HX-Request", "value", r.Header.Get("HX-Request"))
if r.Header.Get("HX-Request") != "true" { if r.Header.Get("HX-Request") != "true" {
ctx = templ.WithChildren(ctx, component) ctx = templ.WithChildren(ctx, component)
component = Base("hello") component = Base("Schreifuchs")
} }
if err := component.Render(ctx, w); err != nil { if err := component.Render(ctx, w); err != nil {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
templ Home(pages []page.PageHeader) { templ Home(pages []page.PageHeader) {
for _, page := range pages { for _, page := range pages {
@components.ImageTile("/images/" + page.CoverImageUID) { @components.ImageTile("/images/" + page.CoverImageUID) {
<a class="flex items-center justify-center h-full " href={ translate.Path(ctx, page.UID) } preload="mouseover"> <a class="flex items-center justify-center h-full " href={ translate.Path(ctx, "/"+page.UID) } preload="mouseover">
<h2 <h2
class="text-3xl font-bold text-white hover:underline text-shadow-lg" class="text-3xl font-bold text-white hover:underline text-shadow-lg"
> >
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
(function(){htmx.defineExtension("preload",{onEvent:function(e,t){if(e==="htmx:afterProcessNode"){const n=t.target||t.detail.elt;const r=[...n.hasAttribute("preload")?[n]:[],...n.querySelectorAll("[preload]")];r.forEach(function(e){i(e);e.querySelectorAll("[href],[hx-get],[data-hx-get]").forEach(i)});return}if(e==="htmx:beforeRequest"){const o=t.detail.requestConfig.headers;if(!("HX-Preloaded"in o&&o["HX-Preloaded"]==="true")){return}t.preventDefault();const a=t.detail.xhr;a.onload=function(){s(t.detail.elt,a.responseText)};a.onerror=null;a.onabort=null;a.ontimeout=null;a.send()}}});function i(t){if(t.preloadState!==undefined){return}if(!l(t)){return}if(t instanceof HTMLFormElement){const o=t;if(!(o.hasAttribute("method")&&o.method==="get"||o.hasAttribute("hx-get")||o.hasAttribute("hx-data-get"))){return}for(let e=0;e<o.elements.length;e++){const a=o.elements.item(e);i(a);if("labels"in a){a.labels.forEach(i)}}return}let e=p(t,"preload");t.preloadAlways=e&&e.includes("always");if(t.preloadAlways){e=e.replace("always","").trim()}let n=e||"mousedown";const r=n==="mouseover";t.addEventListener(n,u(t,r),{passive:true});if(n==="mousedown"||n==="mouseover"){t.addEventListener("touchstart",u(t),{passive:true})}if(n==="mouseover"){t.addEventListener("mouseout",function(e){if(e.target===t&&t.preloadState==="TIMEOUT"){t.preloadState="READY"}},{passive:true})}t.preloadState="READY";htmx.trigger(t,"preload:init")}function u(t,n=false){return function(){if(t.preloadState!=="READY"){return}if(n){t.preloadState="TIMEOUT";const e=100;window.setTimeout(function(){if(t.preloadState==="TIMEOUT"){t.preloadState="READY";r(t)}},e);return}r(t)}}function r(n){if(n.preloadState!=="READY"){return}n.preloadState="LOADING";const e=n.getAttribute("hx-get")||n.getAttribute("data-hx-get");if(e){m(e,n);return}const t=p(n,"hx-boost")==="true";if(n.hasAttribute("href")){const r=n.getAttribute("href");if(t){m(r,n)}else{h(r,n)}return}if(g(n)){const r=n.form.getAttribute("action")||n.form.getAttribute("hx-get")||n.form.getAttribute("data-hx-get");const o=htmx.values(n.form);const a=!(n.form.getAttribute("hx-get")||n.form.getAttribute("data-hx-get")||t);const i=a?h:m;if(n.type==="submit"){i(r,n.form,o);return}const u=n.name||n.control.name;if(n.tagName==="SELECT"){Array.from(n.options).forEach(e=>{if(e.selected)return;o.set(u,e.value);const t=d(n.form,o);i(r,n.form,t)});return}const s=n.getAttribute("type")||n.control.getAttribute("type");const l=n.value||n.control?.value;if(s==="radio"){o.set(u,l)}else if(s==="checkbox"){const c=o.getAll(u);if(c.includes(l)){o[u]=c.filter(e=>e!==l)}else{o.append(u,l)}}const f=d(n.form,o);i(r,n.form,f);return}}function d(e,t){const n=e.elements;const r=new FormData;for(let e=0;e<n.length;e++){const o=n.item(e);if(t.has(o.name)&&o.tagName==="SELECT"){r.append(o.name,t.get(o.name));continue}if(t.has(o.name)&&t.getAll(o.name).includes(o.value)){r.append(o.name,o.value)}}return r}function m(e,t,n=undefined){htmx.ajax("GET",e,{source:t,values:n,headers:{"HX-Preloaded":"true"}})}function h(e,t,n=undefined){const r=new XMLHttpRequest;if(n){e+="?"+new URLSearchParams(n.entries()).toString()}r.open("GET",e);r.setRequestHeader("HX-Preloaded","true");r.onload=function(){s(t,r.responseText)};r.send()}function s(e,t){e.preloadState=e.preloadAlways?"READY":"DONE";if(p(e,"preload-images")==="true"){document.createElement("div").innerHTML=t}}function p(e,t){if(e==undefined){return undefined}return e.getAttribute(t)||e.getAttribute("data-"+t)||p(e.parentElement,t)}function l(e){const n=["href","hx-get","data-hx-get"];const t=t=>n.some(e=>t.hasAttribute(e))||t.method==="get";const r=e.form instanceof HTMLFormElement&&t(e.form)&&g(e);if(!t(e)&&!r){return false}if(e instanceof HTMLInputElement&&e.closest("label")){return false}return true}function g(e){if(e instanceof HTMLInputElement||e instanceof HTMLButtonElement){const t=e.getAttribute("type");return["checkbox","radio","submit"].includes(t)}if(e instanceof HTMLLabelElement){return e.control&&g(e.control)}return e instanceof HTMLSelectElement}})();