feat: title on pages

This commit is contained in:
2026-03-19 18:27:23 +01:00
parent 2b07701762
commit 16e5107185
18 changed files with 251 additions and 36 deletions
+1
View File
@@ -30,6 +30,7 @@ func (s *Service) getMime(path string) (mimeType string, err error) {
if mimer, ok := info.(interface{ ContentType() string }); ok {
mimeType = mimer.ContentType()
return
}
return
@@ -0,0 +1,37 @@
package images_test
import (
"context"
"errors"
"testing"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/test/testdata"
"github.com/valkey-io/valkey-go"
"github.com/valkey-io/valkey-go/mock"
"go.uber.org/mock/gomock"
)
func Setup(ctx context.Context, ctrl *gomock.Controller) (*images.Service, *mock.Client) {
valkey := mock.NewClient(ctrl)
return images.New(testdata.FS(), &config.ImageConfig{Concurency: 1, Quality: 80}, valkey), valkey
}
func BenchmarkService_GetImage(b *testing.B) {
srv, valkeyClient := Setup(b.Context(), gomock.NewController(b))
uid, err := images.UIDFromPath("cover.jpg")
if err != nil {
b.Error("could not get uid", err)
}
b.ReportAllocs()
for b.Loop() {
valkeyClient.EXPECT().Do(gomock.Any(), gomock.Any()).Return(mock.ErrorResult(errors.New("adf")))
valkeyClient.EXPECT().Do(gomock.Any(), gomock.Any()).Return(mock.Result(valkey.ValkeyMessage{}))
_, _, err = srv.GetImage(b.Context(), uid, images.Options{Width: 500, Quality: 80})
if err != nil {
b.Error(err)
}
}
}
+7 -5
View File
@@ -9,6 +9,11 @@ import (
"strings"
)
const (
sourceSetSteps = 300
sourceSetMax = 1500
)
func UIDFromPath(path string) (uid string, err error) {
var b bytes.Buffer
// NewWriter with NoDict (nil) creates a raw DEFLATE compressor
@@ -57,14 +62,11 @@ func PathFromUID(uid string) (string, error) {
func SourceSet(src string) string {
sb := strings.Builder{}
const steps = 300
const max = 1500
i := 1
for {
width := i * steps
width := i * sourceSetSteps
sb.WriteString(fmt.Sprintf("%s?w=%d %dw, ", src, width, width))
if width >= max {
if width >= sourceSetMax {
return sb.String()
}
sb.WriteRune(',')
+3 -1
View File
@@ -16,6 +16,7 @@ type PageHeader struct {
type Page struct {
PageHeader
Content string
Images []Image
}
func (s *Service) GetPages(ctx context.Context) (pages []PageHeader, err error) {
@@ -46,7 +47,7 @@ func (s *Service) GetPage(ctx context.Context, uid string) (page Page, err error
return
}
html, err := s.getHTML(ctx, p)
html, imgs, err := s.getHTML(ctx, p)
if err != nil {
return
}
@@ -54,6 +55,7 @@ func (s *Service) GetPage(ctx context.Context, uid string) (page Page, err error
page = Page{
PageHeader: p,
Content: string(html),
Images: imgs,
}
return
}
+17 -5
View File
@@ -14,7 +14,13 @@ import (
"github.com/gomarkdown/markdown/parser"
)
func (s *Service) getHTML(ctx context.Context, page PageHeader) (out []byte, err error) {
// image can be used to preload
type Image struct {
SrcSet string
Src string
}
func (s *Service) getHTML(ctx context.Context, page PageHeader) (out []byte, imgs []Image, err error) {
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
@@ -24,7 +30,7 @@ func (s *Service) getHTML(ctx context.Context, page PageHeader) (out []byte, err
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{
Flags: htmlFlags,
RenderNodeHook: imageMiddleware(page),
RenderNodeHook: imageMiddleware(page, &imgs),
}
renderer := html.NewRenderer(opts)
@@ -32,7 +38,7 @@ func (s *Service) getHTML(ctx context.Context, page PageHeader) (out []byte, err
return
}
func imageMiddleware(page PageHeader) func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
func imageMiddleware(page PageHeader, imgs *[]Image) func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
return func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
if !entering {
return ast.GoToNext, false
@@ -53,14 +59,20 @@ func imageMiddleware(page PageHeader) func(w io.Writer, node ast.Node, entering
slog.Error("could not get image url", "err", err)
return ast.GoToNext, false
}
image := Image{
SrcSet: images.SourceSet(src),
Src: src + "?w=1024",
}
img.Attribute = &ast.Attribute{Attrs: map[string][]byte{
"srcset": []byte(images.SourceSet(src)),
// "srcset": []byte(image.SrcSet),
"fetchpriority": []byte("low"),
"loading": []byte("lazy"),
"class": []byte("min-h-48"),
"src": []byte(src),
"src": []byte(image.Src),
}}
*imgs = append(*imgs, image)
// // img.Attrs["srcset"] =
}
+13 -1
View File
@@ -1,8 +1,10 @@
package resthome
import (
"fmt"
"net/http"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/templer"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/pages"
)
@@ -17,6 +19,7 @@ func (h *Handler) home(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) page(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid := r.PathValue("uid")
page, err := h.src.GetPage(r.Context(), uid)
@@ -25,5 +28,14 @@ func (h *Handler) page(w http.ResponseWriter, r *http.Request) {
return
}
h.r.Render(r.Context(), w, r, pages.ContentPage(page))
for i, image := range page.Images {
if i > 3 {
break
}
ctx = templer.AppendHeader(ctx, fmt.Sprintf(`<link rel="preload" href="%s" as="image" />`, image.Src))
}
ctx = templer.PageTitle(ctx, page.Title)
h.r.Render(ctx, w, r, pages.ContentPage(page))
}
+48
View File
@@ -0,0 +1,48 @@
package templer
import (
"context"
)
// GetHeader returns a slice of strings that should be added to the html head.
// Because strings are immutable a slice of strings is used.
func GetHeader(ctx context.Context) []string {
v := ctx.Value(headerKey)
if v == nil {
return []string{}
}
header, ok := v.([]string)
if !ok {
return []string{}
}
return header
}
// AppendHeader lets you add lines to the header.
func AppendHeader(ctx context.Context, v ...string) context.Context {
header := GetHeader(ctx)
header = append(header, v...)
return context.WithValue(ctx, headerKey, header)
}
// AppendHeader lets you add lines to the header.
func PageTitle(ctx context.Context, title string) context.Context {
return context.WithValue(ctx, titleKey, title)
}
// AppendHeader lets you add lines to the header.
func GetTitle(ctx context.Context, main string) string {
v := ctx.Value(titleKey)
if v == nil {
return main
}
title, ok := v.(string)
if !ok {
return main
}
return title + " | " + main
}
+2
View File
@@ -11,6 +11,8 @@ type ctxKey int
const (
pathKey ctxKey = iota
headerKey
titleKey
)
func Middleware(next http.Handler) http.HandlerFunc {
+2
View File
@@ -1,3 +1,4 @@
// Package server provides the http server for schreifuchs.ch.
package server
import (
@@ -17,6 +18,7 @@ type Server struct {
cfg config.Cfg
}
// Start starts a new http server. When the context gets canceled, the server stops gracefully.
func Start(ctx context.Context, cfg config.Cfg) (err error) {
port, _ := strconv.Atoi(os.Getenv("PORT"))
if port == 0 {